From 15ac0cb9b8f46acc2b7e3ee48f590c97c04abf6f Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 30 Nov 2024 16:00:48 +0300 Subject: [PATCH] copied the code from the working repo --- .gitignore | 1 + Jenkinsfile | 33 + compose.https.yml | 28 + compose.yml | 181 + mtucijobsbackend/.env.example | 5 + mtucijobsbackend/.gitignore | 3 + mtucijobsbackend/Dockerfile | 6 + mtucijobsbackend/app/__init__.py | 0 mtucijobsbackend/app/config.py | 21 + mtucijobsbackend/app/crud.py | 150 + mtucijobsbackend/app/database.py | 47 + mtucijobsbackend/app/main.py | 32 + mtucijobsbackend/app/models.py | 87 + mtucijobsbackend/app/routers/auth.py | 51 + mtucijobsbackend/app/routers/job.py | 247 + mtucijobsbackend/app/routers/service.py | 55 + mtucijobsbackend/app/routers/student.py | 197 + mtucijobsbackend/app/routers/user.py | 60 + mtucijobsbackend/app/schemas.py | 146 + mtucijobsbackend/app/security.py | 90 + mtucijobsbackend/app/storage.py | 21 + mtucijobsbackend/app/utils.py | 8 + mtucijobsbackend/requirements.txt | 61 + mtucijobsbackend/tests/__init__.py | 0 mtucijobsbackend/tests/conftest.py | 36 + mtucijobsbackend/tests/test_students.py | 158 + mtucijobsbot/.dockerignore | 13 + mtucijobsbot/.env.development | 9 + mtucijobsbot/.gitignore | 4 + mtucijobsbot/Dockerfile | 12 + mtucijobsbot/build.sh | 5 + mtucijobsbot/deploy.sh | 5 + mtucijobsbot/dist/api/resume.js | 40 + mtucijobsbot/dist/assets/шаблон МТУСИ.docx | Bin 0 -> 37363 bytes mtucijobsbot/dist/commands/start.js | 90 + mtucijobsbot/dist/db/db.js | 109 + mtucijobsbot/dist/index.js | 375 + mtucijobsbot/dist/languages/en.json | 24 + mtucijobsbot/dist/languages/ru.json | 24 + mtucijobsbot/dist/models/User.js | 11 + mtucijobsbot/dist/modules/menuController.js | 50 + mtucijobsbot/dist/modules/menuScenes.js | 38 + mtucijobsbot/dist/modules/scenes/accept.js | 112 + mtucijobsbot/dist/modules/scenes/events.js | 292 + mtucijobsbot/dist/modules/scenes/invest.js | 39 + .../dist/modules/scenes/savevacansy.js | 55 + mtucijobsbot/dist/services/messagesService.js | 277 + mtucijobsbot/docker-compose.yml | 62 + mtucijobsbot/package-lock.json | 3007 ++++++++ mtucijobsbot/package.json | 37 + mtucijobsbot/run.sh | 14 + mtucijobsbot/src/api/resume.ts | 34 + mtucijobsbot/src/assets/шаблон МТУСИ.docx | Bin 0 -> 37363 bytes mtucijobsbot/src/commands/start.ts | 90 + mtucijobsbot/src/db/db.ts | 176 + mtucijobsbot/src/index.ts | 469 ++ mtucijobsbot/src/languages/en.json | 24 + mtucijobsbot/src/languages/ru.json | 24 + mtucijobsbot/src/models/User.ts | 23 + mtucijobsbot/src/modules/menuController.ts | 43 + mtucijobsbot/src/modules/menuScenes.ts | 39 + mtucijobsbot/src/modules/scenes/accept.ts | 107 + mtucijobsbot/src/modules/scenes/events.ts | 366 + .../src/modules/scenes/savevacansy.ts | 49 + mtucijobsbot/src/services/messagesService.ts | 371 + mtucijobsbot/tsconfig.json | 109 + mtucijobsweb/.dockerignore | 12 + mtucijobsweb/.eslintrc.js | 4 + mtucijobsweb/.gitignore | 35 + mtucijobsweb/Dockerfile | 18 + mtucijobsweb/README.md | 36 + mtucijobsweb/api/api.ts | 77 + mtucijobsweb/api/axiosInstance.ts | 10 + mtucijobsweb/api/bot.ts | 15 + mtucijobsweb/app/layout.tsx | 27 + mtucijobsweb/app/page.tsx | 20 + mtucijobsweb/app/search/page.tsx | 15 + mtucijobsweb/build.sh | 7 + .../fsd/app/providers/ClientProvider.tsx | 14 + .../fsd/app/providers/TmaSDKLoader.tsx | 13 + mtucijobsweb/fsd/entities/Resume/data.ts | 109 + mtucijobsweb/fsd/pages/Loading.tsx | 15 + mtucijobsweb/fsd/pages/MainPage.tsx | 17 + mtucijobsweb/fsd/pages/SearchPage.tsx | 13 + .../fsd/widgets/Resume/Resume.module.scss | 39 + mtucijobsweb/fsd/widgets/Resume/Resume.tsx | 403 ++ .../widgets/Search/SearchWidget.module.scss | 38 + .../fsd/widgets/Search/SearchWidget.tsx | 175 + mtucijobsweb/next.config.js | 10 + mtucijobsweb/package-lock.json | 6178 +++++++++++++++++ mtucijobsweb/package.json | 35 + mtucijobsweb/postcss.config.js | 6 + mtucijobsweb/public/next.svg | 1 + mtucijobsweb/public/vercel.svg | 1 + mtucijobsweb/run.sh | 15 + mtucijobsweb/tailwind.config.ts | 20 + mtucijobsweb/tsconfig.json | 27 + mtucijobsweb/types/types.ts | 60 + mtucijobsweb2/.eslintrc.json | 4 + mtucijobsweb2/.gitignore | 35 + mtucijobsweb2/Dockerfile | 16 + mtucijobsweb2/README.md | 36 + mtucijobsweb2/api/api.ts | 161 + mtucijobsweb2/api/axiosInstance.ts | 31 + mtucijobsweb2/app/create/page.tsx | 7 + mtucijobsweb2/app/editvacansy/[id]/page.tsx | 9 + mtucijobsweb2/app/layout.tsx | 36 + mtucijobsweb2/app/login/page.tsx | 7 + mtucijobsweb2/app/page.tsx | 12 + mtucijobsweb2/app/resume/[id]/page.tsx | 8 + mtucijobsweb2/app/search/page.tsx | 11 + mtucijobsweb2/app/view/page.tsx | 11 + .../fsd/app/provider/AuthContext.tsx | 56 + mtucijobsweb2/fsd/app/provider/AuthGuard.tsx | 30 + .../fsd/app/provider/QueryClient.tsx | 14 + mtucijobsweb2/fsd/app/provider/withAuth.tsx | 25 + mtucijobsweb2/fsd/app/styles/global.scss | 182 + mtucijobsweb2/fsd/pages/EditVacancyPage.tsx | 15 + mtucijobsweb2/fsd/pages/LoginPage.tsx | 12 + mtucijobsweb2/fsd/pages/RedirectPage.tsx | 12 + mtucijobsweb2/fsd/pages/ResumePage.tsx | 15 + mtucijobsweb2/fsd/pages/SearchResumePage.tsx | 13 + mtucijobsweb2/fsd/pages/VacansyPage.tsx | 12 + mtucijobsweb2/fsd/pages/ViewVacansyPage.tsx | 14 + .../fsd/widgets/Header/Header.module.scss | 69 + mtucijobsweb2/fsd/widgets/Header/Header.tsx | 147 + .../fsd/widgets/Login/Login.module.scss | 6 + mtucijobsweb2/fsd/widgets/Login/Login.tsx | 96 + .../fsd/widgets/Redirect/Redirect.tsx | 22 + .../fsd/widgets/Resume/Resume.module.scss | 13 + mtucijobsweb2/fsd/widgets/Resume/Resume.tsx | 110 + .../SearchResume/SearchResume.module.scss | 51 + .../fsd/widgets/SearchResume/SearchResume.tsx | 149 + .../fsd/widgets/Vacansy/Vacansy.module.scss | 25 + mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx | 211 + .../EditVacancy/EditVacancy.module.scss | 15 + .../ViewVacansy/EditVacancy/EditVacancy.tsx | 141 + .../ViewVacansy/ViewVacansy.module.scss | 44 + .../fsd/widgets/ViewVacansy/ViewVacansy.tsx | 206 + mtucijobsweb2/next.config.js | 9 + mtucijobsweb2/package-lock.json | 5255 ++++++++++++++ mtucijobsweb2/package.json | 31 + mtucijobsweb2/public/backgorund.png | Bin 0 -> 16334 bytes mtucijobsweb2/public/favicon.svg | 57 + mtucijobsweb2/public/next.svg | 1 + mtucijobsweb2/public/vercel.svg | 1 + mtucijobsweb2/tsconfig.json | 27 + mtucijobsweb2/types/types.ts | 55 + 148 files changed, 23342 insertions(+) create mode 100644 .gitignore create mode 100644 Jenkinsfile create mode 100644 compose.https.yml create mode 100644 compose.yml create mode 100644 mtucijobsbackend/.env.example create mode 100644 mtucijobsbackend/.gitignore create mode 100644 mtucijobsbackend/Dockerfile create mode 100644 mtucijobsbackend/app/__init__.py create mode 100644 mtucijobsbackend/app/config.py create mode 100644 mtucijobsbackend/app/crud.py create mode 100644 mtucijobsbackend/app/database.py create mode 100644 mtucijobsbackend/app/main.py create mode 100644 mtucijobsbackend/app/models.py create mode 100644 mtucijobsbackend/app/routers/auth.py create mode 100644 mtucijobsbackend/app/routers/job.py create mode 100644 mtucijobsbackend/app/routers/service.py create mode 100644 mtucijobsbackend/app/routers/student.py create mode 100644 mtucijobsbackend/app/routers/user.py create mode 100644 mtucijobsbackend/app/schemas.py create mode 100644 mtucijobsbackend/app/security.py create mode 100644 mtucijobsbackend/app/storage.py create mode 100644 mtucijobsbackend/app/utils.py create mode 100644 mtucijobsbackend/requirements.txt create mode 100644 mtucijobsbackend/tests/__init__.py create mode 100644 mtucijobsbackend/tests/conftest.py create mode 100644 mtucijobsbackend/tests/test_students.py create mode 100644 mtucijobsbot/.dockerignore create mode 100644 mtucijobsbot/.env.development create mode 100644 mtucijobsbot/.gitignore create mode 100644 mtucijobsbot/Dockerfile create mode 100644 mtucijobsbot/build.sh create mode 100644 mtucijobsbot/deploy.sh create mode 100644 mtucijobsbot/dist/api/resume.js create mode 100644 mtucijobsbot/dist/assets/шаблон МТУСИ.docx create mode 100644 mtucijobsbot/dist/commands/start.js create mode 100644 mtucijobsbot/dist/db/db.js create mode 100644 mtucijobsbot/dist/index.js create mode 100644 mtucijobsbot/dist/languages/en.json create mode 100644 mtucijobsbot/dist/languages/ru.json create mode 100644 mtucijobsbot/dist/models/User.js create mode 100644 mtucijobsbot/dist/modules/menuController.js create mode 100644 mtucijobsbot/dist/modules/menuScenes.js create mode 100644 mtucijobsbot/dist/modules/scenes/accept.js create mode 100644 mtucijobsbot/dist/modules/scenes/events.js create mode 100644 mtucijobsbot/dist/modules/scenes/invest.js create mode 100644 mtucijobsbot/dist/modules/scenes/savevacansy.js create mode 100644 mtucijobsbot/dist/services/messagesService.js create mode 100644 mtucijobsbot/docker-compose.yml create mode 100644 mtucijobsbot/package-lock.json create mode 100644 mtucijobsbot/package.json create mode 100644 mtucijobsbot/run.sh create mode 100644 mtucijobsbot/src/api/resume.ts create mode 100644 mtucijobsbot/src/assets/шаблон МТУСИ.docx create mode 100644 mtucijobsbot/src/commands/start.ts create mode 100644 mtucijobsbot/src/db/db.ts create mode 100644 mtucijobsbot/src/index.ts create mode 100644 mtucijobsbot/src/languages/en.json create mode 100644 mtucijobsbot/src/languages/ru.json create mode 100644 mtucijobsbot/src/models/User.ts create mode 100644 mtucijobsbot/src/modules/menuController.ts create mode 100644 mtucijobsbot/src/modules/menuScenes.ts create mode 100644 mtucijobsbot/src/modules/scenes/accept.ts create mode 100644 mtucijobsbot/src/modules/scenes/events.ts create mode 100644 mtucijobsbot/src/modules/scenes/savevacansy.ts create mode 100644 mtucijobsbot/src/services/messagesService.ts create mode 100644 mtucijobsbot/tsconfig.json create mode 100644 mtucijobsweb/.dockerignore create mode 100644 mtucijobsweb/.eslintrc.js create mode 100644 mtucijobsweb/.gitignore create mode 100644 mtucijobsweb/Dockerfile create mode 100644 mtucijobsweb/README.md create mode 100644 mtucijobsweb/api/api.ts create mode 100644 mtucijobsweb/api/axiosInstance.ts create mode 100644 mtucijobsweb/api/bot.ts create mode 100644 mtucijobsweb/app/layout.tsx create mode 100644 mtucijobsweb/app/page.tsx create mode 100644 mtucijobsweb/app/search/page.tsx create mode 100644 mtucijobsweb/build.sh create mode 100644 mtucijobsweb/fsd/app/providers/ClientProvider.tsx create mode 100644 mtucijobsweb/fsd/app/providers/TmaSDKLoader.tsx create mode 100644 mtucijobsweb/fsd/entities/Resume/data.ts create mode 100644 mtucijobsweb/fsd/pages/Loading.tsx create mode 100644 mtucijobsweb/fsd/pages/MainPage.tsx create mode 100644 mtucijobsweb/fsd/pages/SearchPage.tsx create mode 100644 mtucijobsweb/fsd/widgets/Resume/Resume.module.scss create mode 100644 mtucijobsweb/fsd/widgets/Resume/Resume.tsx create mode 100644 mtucijobsweb/fsd/widgets/Search/SearchWidget.module.scss create mode 100644 mtucijobsweb/fsd/widgets/Search/SearchWidget.tsx create mode 100644 mtucijobsweb/next.config.js create mode 100644 mtucijobsweb/package-lock.json create mode 100644 mtucijobsweb/package.json create mode 100644 mtucijobsweb/postcss.config.js create mode 100644 mtucijobsweb/public/next.svg create mode 100644 mtucijobsweb/public/vercel.svg create mode 100644 mtucijobsweb/run.sh create mode 100644 mtucijobsweb/tailwind.config.ts create mode 100644 mtucijobsweb/tsconfig.json create mode 100644 mtucijobsweb/types/types.ts create mode 100644 mtucijobsweb2/.eslintrc.json create mode 100644 mtucijobsweb2/.gitignore create mode 100644 mtucijobsweb2/Dockerfile create mode 100644 mtucijobsweb2/README.md create mode 100644 mtucijobsweb2/api/api.ts create mode 100644 mtucijobsweb2/api/axiosInstance.ts create mode 100644 mtucijobsweb2/app/create/page.tsx create mode 100644 mtucijobsweb2/app/editvacansy/[id]/page.tsx create mode 100644 mtucijobsweb2/app/layout.tsx create mode 100644 mtucijobsweb2/app/login/page.tsx create mode 100644 mtucijobsweb2/app/page.tsx create mode 100644 mtucijobsweb2/app/resume/[id]/page.tsx create mode 100644 mtucijobsweb2/app/search/page.tsx create mode 100644 mtucijobsweb2/app/view/page.tsx create mode 100644 mtucijobsweb2/fsd/app/provider/AuthContext.tsx create mode 100644 mtucijobsweb2/fsd/app/provider/AuthGuard.tsx create mode 100644 mtucijobsweb2/fsd/app/provider/QueryClient.tsx create mode 100644 mtucijobsweb2/fsd/app/provider/withAuth.tsx create mode 100644 mtucijobsweb2/fsd/app/styles/global.scss create mode 100644 mtucijobsweb2/fsd/pages/EditVacancyPage.tsx create mode 100644 mtucijobsweb2/fsd/pages/LoginPage.tsx create mode 100644 mtucijobsweb2/fsd/pages/RedirectPage.tsx create mode 100644 mtucijobsweb2/fsd/pages/ResumePage.tsx create mode 100644 mtucijobsweb2/fsd/pages/SearchResumePage.tsx create mode 100644 mtucijobsweb2/fsd/pages/VacansyPage.tsx create mode 100644 mtucijobsweb2/fsd/pages/ViewVacansyPage.tsx create mode 100644 mtucijobsweb2/fsd/widgets/Header/Header.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/Header/Header.tsx create mode 100644 mtucijobsweb2/fsd/widgets/Login/Login.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/Login/Login.tsx create mode 100644 mtucijobsweb2/fsd/widgets/Redirect/Redirect.tsx create mode 100644 mtucijobsweb2/fsd/widgets/Resume/Resume.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/Resume/Resume.tsx create mode 100644 mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.tsx create mode 100644 mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx create mode 100644 mtucijobsweb2/fsd/widgets/ViewVacansy/EditVacancy/EditVacancy.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/ViewVacansy/EditVacancy/EditVacancy.tsx create mode 100644 mtucijobsweb2/fsd/widgets/ViewVacansy/ViewVacansy.module.scss create mode 100644 mtucijobsweb2/fsd/widgets/ViewVacansy/ViewVacansy.tsx create mode 100644 mtucijobsweb2/next.config.js create mode 100644 mtucijobsweb2/package-lock.json create mode 100644 mtucijobsweb2/package.json create mode 100644 mtucijobsweb2/public/backgorund.png create mode 100644 mtucijobsweb2/public/favicon.svg create mode 100644 mtucijobsweb2/public/next.svg create mode 100644 mtucijobsweb2/public/vercel.svg create mode 100644 mtucijobsweb2/tsconfig.json create mode 100644 mtucijobsweb2/types/types.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f509e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.env \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..945695f --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,33 @@ +pipeline { + agent any + + environment { + PROD_ENV = credentials('prod_env') + DEV_ENV = credentials('dev_env') + } + + stages { + stage('Build') { + steps { + withCredentials([file(credentialsId: 'dev_env', variable: 'DEV_ENV_FILE')]) { + sh 'rm -f ${WORKSPACE}/.env' + sh 'cp ${DEV_ENV_FILE} ./.env' + } + + sh 'docker compose build' + } + } + + stage('Deploy for development') { + agent { node { label "dev" } } + + when { + branch 'dev' + } + + steps { + sh 'docker compose up -d' + } + } + } +} \ No newline at end of file diff --git a/compose.https.yml b/compose.https.yml new file mode 100644 index 0000000..c8d6ae3 --- /dev/null +++ b/compose.https.yml @@ -0,0 +1,28 @@ +services: + traefik: + image: traefik:v2.11 + container_name: "traefik" + restart: always + command: + - "--log.level=DEBUG" + - "--accesslog=true" + - "--providers.docker=true" + - "--providers.docker.network=proxy" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entryPoints.web.http.redirections.entrypoint.scheme=https" + ports: + - "80:80" + - "443:443" + volumes: + - traefik:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + traefik: + +networks: + default: + name: proxy \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..c86c11a --- /dev/null +++ b/compose.yml @@ -0,0 +1,181 @@ +services: + bot: + image: mtuci-jobs/bot:0.1 + container_name: bot + build: + context: mtucijobsbot + restart: always + depends_on: + postgres-bot: + condition: service_healthy + environment: + - DATABASE_URL=${db_bot_url} + - DB_NAME=${db_bot_name} + - DB_USER=${db_bot_user} + - DB_PASSWORD=${db_bot_password} + - DB_HOST=${db_bot_host} + - BOT_TOKEN=${bot_token} + - HOOKPORT=${hookport} + - PORT=${port} + - DOMAIN=${domain} + - WEB_APP=https://web.${domain}/ + - WEB_APP_SEARCH=https://web.${domain}/search + - API=https://${domain}/api/ + - MTUCI_TECH=${mtuci_tech} + - BEARER_TOKEN=${mtuci_tech_token} + networks: + - frontend + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.bot.rule=Host(`${domain}`)" + - "traefik.http.routers.bot.entrypoints=websecure" + - "traefik.http.routers.bot.tls.certresolver=${certresolver}" + - "traefik.http.services.bot.loadbalancer.server.port=3005" + - "traefik.http.routers.bot.service=bot" + - "traefik.http.routers.bot-resume.rule=Host(`${domain}`) && Path(`/api/resume/`)" + - "traefik.http.routers.bot-resume.service=bot-resume" + - "traefik.http.routers.bot-resume.entrypoints=websecure" + - "traefik.http.routers.bot-resume.tls.certresolver=${certresolver}" + - "traefik.http.services.bot-resume.loadbalancer.server.port=3006" + + postgres-bot: + image: postgres:16.1-alpine3.19 + container_name: postgres-bot + restart: always + environment: + - POSTGRES_USER=${db_bot_user} + - POSTGRES_PASSWORD=${db_bot_password} + - POSTGRES_DB=${db_bot_name} + - PGDATA=${db_bot_pgdata} + - POSTGRES_HOST_AUTH_METHOD=${db_bot_auth} + healthcheck: + test: pg_isready -U postgres + interval: 5s + timeout: 5s + retries: 5 + volumes: + - postgres-bot:/data/postgres + networks: + - frontend + + web: + container_name: web + image: mtuci-jobs/web:0.1 + build: + context: mtucijobsweb + args: + - NEXT_PUBLIC_APP_BASE_URL=https://${domain}/api + - NEXT_PUBLIC_BOT_URL=https://${domain} + restart: always + networks: + - frontend + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.web.rule=Host(`web.${domain}`)" + - "traefik.http.routers.web.entrypoints=websecure" + - "traefik.http.routers.web.tls.certresolver=myresolver" + - "traefik.http.services.web.loadbalancer.server.port=3000" + + web-jobs: + container_name: web-jobs + image: mtuci-jobs/web-jobs:0.1 + build: + context: mtucijobsweb2 + args: + - APP_BASE_URL=https://${domain}/api + restart: always + networks: + - frontend + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.web-jobs.rule=Host(`web-jobs.${domain}`)" + - "traefik.http.routers.web-jobs.entrypoints=websecure" + - "traefik.http.routers.web-jobs.tls.certresolver=myresolver" + - "traefik.http.services.web-jobs.loadbalancer.server.port=3000" + + backend: + container_name: backend + image: mtuci-jobs/backend:0.1 + build: + context: mtucijobsbackend + restart: always + environment: + - database_hostname=${db_backend_hostname} + - database_port=${db_backend_port} + - database_password=${db_backend_password} + - database_name=${db_backend_name} + - database_username=${db_backend_username} + - access_key=${minio_access_key} + - secret_key=${minio_secret_key} + - endpoint=${minio_endpoint} + - secret_key2=${secret_key2} + - algorithm=${algorithm} + - access_token_expire_minutes=${access_token_expire_minutes} + - refresh_token_expire_days=${refresh_token_expire_days} + - x_api_key=${x_api_key} + - LOG_LEVEL=DEBUG + depends_on: + - postgres-backend + - minio + networks: + - backend + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.backend.rule=Host(`${domain}`) && PathPrefix(`/api/`)" + - "traefik.http.middlewares.api-prefix.stripprefix.prefixes=/api/" + - "traefik.http.routers.backend.entrypoints=websecure" + - "traefik.http.routers.backend.tls.certresolver=myresolver" + - "traefik.http.services.backend.loadbalancer.server.port=8000" + - "traefik.http.routers.backend.middlewares=api-prefix" + + postgres-backend: + container_name: postgres-backend + image: postgres:16.1-alpine3.19 + restart: always + environment: + - POSTGRES_PASSWORD=${db_backend_password} + - POSTGRES_DB=${db_backend_name} + healthcheck: + test: pg_isready -U postgres + interval: 5s + timeout: 5s + retries: 5 + volumes: + - postgres-backend:/var/lib/postgresql/data + networks: + - backend + + minio: + container_name: minio + image: minio/minio:latest + command: server --console-address ":9001" /data/ + restart: always + environment: + MINIO_ROOT_USER: ${minio_access_key} + MINIO_ROOT_PASSWORD: ${minio_secret_key} + volumes: + - minio-storage:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + networks: + - backend + +volumes: + postgres-backend: + minio-storage: + postgres-bot: + +networks: + backend: + external: false + frontend: + external: false + proxy: + external: true \ No newline at end of file diff --git a/mtucijobsbackend/.env.example b/mtucijobsbackend/.env.example new file mode 100644 index 0000000..0d6a385 --- /dev/null +++ b/mtucijobsbackend/.env.example @@ -0,0 +1,5 @@ +database_hostname = +database_port = +database_password = +database_name = +database_username = \ No newline at end of file diff --git a/mtucijobsbackend/.gitignore b/mtucijobsbackend/.gitignore new file mode 100644 index 0000000..cd4a30a --- /dev/null +++ b/mtucijobsbackend/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +venv/ +.env \ No newline at end of file diff --git a/mtucijobsbackend/Dockerfile b/mtucijobsbackend/Dockerfile new file mode 100644 index 0000000..749399e --- /dev/null +++ b/mtucijobsbackend/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.9-alpine +WORKDIR /code +COPY ./requirements.txt /code/requirements.txt +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +COPY . . +CMD ["fastapi", "run", "./app/main.py", "--root-path", "/api/"] \ No newline at end of file diff --git a/mtucijobsbackend/app/__init__.py b/mtucijobsbackend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mtucijobsbackend/app/config.py b/mtucijobsbackend/app/config.py new file mode 100644 index 0000000..b754a06 --- /dev/null +++ b/mtucijobsbackend/app/config.py @@ -0,0 +1,21 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_hostname: str + database_port: str + database_password: str + database_name: str + database_username: str + access_key: str + secret_key: str + endpoint: str + secret_key2: str + algorithm: str + access_token_expire_minutes: int + refresh_token_expire_days: int + x_api_key: str + + class Config: + env_file = ".env" + +settings = Settings() \ No newline at end of file diff --git a/mtucijobsbackend/app/crud.py b/mtucijobsbackend/app/crud.py new file mode 100644 index 0000000..1c77612 --- /dev/null +++ b/mtucijobsbackend/app/crud.py @@ -0,0 +1,150 @@ +from fastapi import status, HTTPException, Depends, BackgroundTasks +from sqlalchemy.orm import Session +from typing import List +from sqlalchemy import func +from . import models +from typing import List, Dict +from datetime import date +import requests + +def get_group_pattern(year): + today = date.today() + current_year = int(today.strftime("%Y")) + current_month = int(today.strftime("%m")) + + if current_month > 5: + year -= 1 + + if year <= 0: + return "_" + + pattern = "^[:alpha:]" + + for i in range(year): + pattern += "|" + str(current_year % 100 - i) + + return pattern + +def send_webhook_background(event_type: str, data: Dict): + webhook_url = "http://bot:3006/webhook" + payload = { + "event": event_type, + "data": data + } + try: + response = requests.post(webhook_url, json=payload) + response.raise_for_status() + except requests.RequestException as e: + print(f"Failed to send webhook: {e}") + +def send_webhook_response_updated(data: Dict): + event_type = "response_updated" + send_webhook_background(event_type, data) + +def update_hardskills(entity_id: int, hardskills: List[str], is_job: bool, db: Session): + model = models.JobsHard_skills if is_job else models.StudentsHard_skills + id_field = 'JobID' if is_job else 'StudentID' + + # Удаление существующих навыков + db.query(model).filter(getattr(model, id_field) == entity_id).delete() + db.flush() + + # Добавление новых навыков + hardskills_query = db.query(models.Hard_skills).filter(models.Hard_skills.Title.in_(hardskills)) + new_hardskills = [model(**{id_field: entity_id, 'Hard_skillID': hardskill.Hard_skillID}) for hardskill in hardskills_query] + db.add_all(new_hardskills) + db.flush() + +def update_matches(entity_id: int, is_job: bool, db: Session, background_tasks: BackgroundTasks) -> List[Dict]: + if is_job: + matches = db.query( + models.JobsHard_skills.JobID, + models.StudentsHard_skills.StudentID, + func.count(models.StudentsHard_skills.Hard_skillID).label("count_students_skills") + ).join(models.JobsHard_skills, models.JobsHard_skills.Hard_skillID == models.StudentsHard_skills.Hard_skillID).\ + filter(models.JobsHard_skills.JobID == entity_id).\ + group_by(models.JobsHard_skills.JobID, models.StudentsHard_skills.StudentID).\ + all() + + count_jobs_skills = db.query(func.count(models.JobsHard_skills.Hard_skillID)).filter(models.JobsHard_skills.JobID == entity_id).scalar() + db.query(models.Matches).filter(models.Matches.JobID == entity_id).delete() + else: + matches = db.query( + models.StudentsHard_skills.StudentID, + models.JobsHard_skills.JobID, + func.count(models.StudentsHard_skills.Hard_skillID).label("count_students_skills") + ).join(models.Jobs, models.Jobs.JobID == models.JobsHard_skills.JobID).\ + join(models.StudentsHard_skills, models.JobsHard_skills.Hard_skillID == models.StudentsHard_skills.Hard_skillID).\ + filter(models.Jobs.Archive == False, models.StudentsHard_skills.StudentID == entity_id).\ + group_by(models.StudentsHard_skills.StudentID, models.JobsHard_skills.JobID).\ + all() + + db.query(models.Matches).filter(models.Matches.StudentID == entity_id).delete() + + db.flush() + + updated_matches = [] + for match in matches: + if is_job: + MATCH = match.count_students_skills / count_jobs_skills * 100 + updated_matches.append({ + "job_id": match.JobID, + "student_id": match.StudentID, + "match_percentage": MATCH + }) + else: + count_jobs_skills = db.query(func.count(models.JobsHard_skills.Hard_skillID)).filter(models.JobsHard_skills.JobID == match.JobID).scalar() + MATCH = match.count_students_skills / count_jobs_skills * 100 + updated_matches.append({ + "job_id": match.JobID, + "student_id": match.StudentID, + "match_percentage": MATCH + }) + + new_match = models.Matches(StudentID=match.StudentID, JobID=match.JobID, Match=MATCH) + db.add(new_match) + + db.flush() + + if updated_matches: + background_tasks.add_task(send_webhook_background, "matches_updated", { + "entity_type": "job" if is_job else "student", + "entity_id": entity_id, + "updated_matches": updated_matches + }) + + return updated_matches + +def update_record(model, model_id: int, updated_data: dict, db: Session, background_tasks: BackgroundTasks): + query = db.query(model).filter(model.ResponseID == model_id) + record = query.first() + + if not record: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + query.update(updated_data, synchronize_session=False) + db.flush() + + commit_transaction(db) + + post_update_data = { + "response_id": record.ResponseID, + "student_id": record.StudentID, + "job_id": record.JobID, + "status": record.Status, + "comment": record.Comment, + "link": record.Link + } + + if post_update_data: + background_tasks.add_task(send_webhook_response_updated, post_update_data) + + return query.first() + +def commit_transaction(db): + try: + db.commit() + except: + db.rollback() + raise HTTPException(status_code=status.HTTP_409_CONFLICT) + \ No newline at end of file diff --git a/mtucijobsbackend/app/database.py b/mtucijobsbackend/app/database.py new file mode 100644 index 0000000..c3c37e6 --- /dev/null +++ b/mtucijobsbackend/app/database.py @@ -0,0 +1,47 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session +from .config import settings +from .models import Hard_skills + +DATABASE_URL = f'postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}:{settings.database_port}/{settings.database_name}' + +engine = create_engine(DATABASE_URL) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def get_db(): + db = SessionLocal() + + try: + yield db + finally: + db.close() + +def init_db(session: Session): + + titles = [ + 'C', 'C++', 'C#', 'Java', 'Python', 'Ruby', 'Ruby on Rails', 'R', 'Matlab', 'Django', 'NetBeans', + 'Scala', 'JavaScript', 'TypeScript', 'Go', 'Software Development', 'Application Development', + 'Web Applications', 'Object Oriented Programming', 'Aspect Oriented Programming', 'Concurrent Programming', + 'Mobile Development (iOS)', 'Mobile Development (Android)', 'Data Science', 'Data Analytics', 'Data Mining', + 'Data Visualization', 'Machine Learning', 'TensorFlow', 'PyTorch', 'Keras', 'Theano', 'Statistical Analysis', + 'Bayesian Analysis', 'Regression Analysis', 'Time Series Analysis', 'Clustering', 'K-means', 'KNN', 'Decision Trees', + 'Random Forest', 'Dimensionality Reduction', 'PCA', 'SVD', 'Gradient Descent', 'Stochastic Gradient Descent', + 'Outlier Detection', 'Frequent Itemset Mining', 'SQL', 'NoSQL', 'SQL Server', 'MS SQL Server', 'Apache Hadoop', + 'Apache Spark', 'Apache Airflow', 'Apache Impala', 'Apache Drill', 'HTML', 'CSS', 'React', 'Angular', 'Vue.js', + 'Node.js', 'Express.js', 'REST', 'SOAP', 'Web Platforms', 'System Architecture', 'Distributed Computing', 'AWS', + 'AWS Glue', 'Azure', 'Google Cloud Platform', 'Docker', 'Kubernetes', 'UNIX', 'Linux', 'Windows', 'MacOS', + 'Embedded Hardware', 'Debugging', 'Unit Testing', 'Integration Testing', 'System Testing', 'Code Review', 'Git', + 'SVN', 'CI/CD', 'Software Documentation', 'IDE', 'CASE Tools', 'Computational Complexity', 'Algorithm Design', + 'Data Structures', 'Mathematical Modeling', 'Statistics', 'Technical Writing', 'Technical Support', 'System Design', + 'System Development', 'Technical Guidance', 'Client Interface', 'Vendor Interface', 'Emerging Technologies', 'Jira', + 'Trello', 'Software Architecture', 'Word', 'Excel' + ] + + for title in titles: + exist_hardskill = session.query(Hard_skills).filter(Hard_skills.Title == title).first() + + if not exist_hardskill: + hard_skills_object = Hard_skills(Title=title) + session.add(hard_skills_object) + session.commit() \ No newline at end of file diff --git a/mtucijobsbackend/app/main.py b/mtucijobsbackend/app/main.py new file mode 100644 index 0000000..17f987b --- /dev/null +++ b/mtucijobsbackend/app/main.py @@ -0,0 +1,32 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy.orm import Session +from .routers import auth, user, service, student, job +from .database import init_db, engine +from .models import Base + +Base.metadata.create_all(engine) + +with Session(engine) as session: + init_db(session) + +app = FastAPI() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.include_router(auth.router) +app.include_router(user.router) +app.include_router(student.router) +app.include_router(job.router) +app.include_router(service.router) + +@app.get("/") +def root(): + return {"message": "I'm ok!"} \ No newline at end of file diff --git a/mtucijobsbackend/app/models.py b/mtucijobsbackend/app/models.py new file mode 100644 index 0000000..1d5ee8b --- /dev/null +++ b/mtucijobsbackend/app/models.py @@ -0,0 +1,87 @@ +from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, ARRAY, UniqueConstraint +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): pass + +class Users(Base): + __tablename__ = 'Users' + UserID = Column(Integer, primary_key=True, autoincrement=True) + Email = Column(String(166), nullable=False, unique=True) + Hashed_password = Column(String(200), nullable=False) + +class Students(Base): + __tablename__ = 'Students' + StudentID = Column(Integer, primary_key=True, autoincrement=True) + Name = Column(String(155), nullable=False) + Type = Column(String(60), nullable=False) + Faculties = Column(String(70), nullable=False) + Group = Column(String(20), nullable=False) + Year = Column(Integer, nullable=False) + Experience_specialty = Column(Boolean, nullable=False) + Time = Column(ARRAY(String(3)), nullable=False) + Soft_skills = Column(String(155), nullable=False) + Link = Column(String(155), nullable=False) + Email = Column(String(166), nullable=False) + Phone_number = Column(String(16), nullable=False) + +class Jobs(Base): + __tablename__ = 'Jobs' + JobID = Column(Integer, primary_key=True, autoincrement=True) + UserID = Column(Integer, ForeignKey( + "Users.UserID", ondelete="CASCADE"), nullable=False) + Company_name = Column(String(155), nullable=False) + Link_to_job = Column(String(155), nullable=True) + Job_name = Column(String(155), nullable=False) + Year = Column(String(1), nullable=False) + Qualification = Column(Boolean, nullable=False) + Salary_after_interview = Column(Boolean, nullable=False) + Salary = Column(Integer, nullable=False) + Email = Column(String(155), nullable=False) + Archive = Column(Boolean, nullable=False) + Responsibilities = Column(String(255), nullable=False) + Time = Column(ARRAY(String), nullable=False) + +class Hard_skills(Base): + __tablename__ = 'Hard_skills' + Hard_skillID = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + Title = Column(String, nullable=False, unique=True) + +class StudentsHard_skills(Base): + __tablename__ = 'StudentsHard_skills' + + StudentID = Column(Integer, ForeignKey( + "Students.StudentID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) + Hard_skillID = Column(Integer, ForeignKey( + "Hard_skills.Hard_skillID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) + +class JobsHard_skills(Base): + __tablename__ = 'JobsHard_skills' + + JobID = Column(Integer, ForeignKey( + "Jobs.JobID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) + Hard_skillID = Column(Integer, ForeignKey( + "Hard_skills.Hard_skillID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) + +class Responses(Base): + __tablename__ = 'Responses' + + ResponseID = Column(Integer, primary_key=True, autoincrement=True) + StudentID = Column(Integer, ForeignKey( + "Students.StudentID", onupdate="CASCADE", ondelete="CASCADE")) + JobID = Column(Integer, ForeignKey( + "Jobs.JobID", onupdate="CASCADE", ondelete="CASCADE")) + Status = Column(String(50), nullable=True) + Comment = Column(String(700), nullable=True) + Link = Column(String(155), nullable=True) + + __table_args__ = ( + UniqueConstraint('StudentID', 'JobID', name='unique_student_job_for_responses'), + ) + +class Matches(Base): + __tablename__ = 'Matches' + StudentID = Column(Integer, ForeignKey( + "Students.StudentID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) + JobID = Column(Integer, ForeignKey( + "Jobs.JobID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) + Match = Column(Integer, nullable=False) \ No newline at end of file diff --git a/mtucijobsbackend/app/routers/auth.py b/mtucijobsbackend/app/routers/auth.py new file mode 100644 index 0000000..46f6961 --- /dev/null +++ b/mtucijobsbackend/app/routers/auth.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Depends, status, HTTPException, Response +from fastapi.security.oauth2 import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session + +from .. import database, schemas, models, security, utils + +router = APIRouter(tags=['Authentication']) + + +@router.post('/login', response_model=schemas.Token) +def login(user_credentials: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)): + + user = db.query(models.Users).filter( + models.Users.Email == user_credentials.username).first() + + if not user: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials") + + if not utils.verify(user_credentials.password, user.Hashed_password): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials") + + access_token = security.create_access_token(data={ + "UserID": user.UserID, + "Email": user.Email + }) + + refresh_token = security.create_refresh_token(data={ + "UserID": user.UserID, + }) + + return {"access_token": access_token, "refresh_token": refresh_token} + +@router.post('/refresh', response_model=schemas.Token, response_model_exclude_none=True ) +def refresh_access_token(refresh_token: str, db: Session = Depends(database.get_db)): + user_id = security.verify_refresh_token(refresh_token) + + user = db.query(models.Users).filter( + models.Users.UserID == user_id).first() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + + access_token = security.create_access_token(data={ + "UserID": user.UserID, + "Email": user.Email + }) + + return {"access_token": access_token} \ No newline at end of file diff --git a/mtucijobsbackend/app/routers/job.py b/mtucijobsbackend/app/routers/job.py new file mode 100644 index 0000000..eae608f --- /dev/null +++ b/mtucijobsbackend/app/routers/job.py @@ -0,0 +1,247 @@ +from fastapi import status, HTTPException, Response, APIRouter, Depends, BackgroundTasks, Query +from sqlalchemy.orm import Session +from sqlalchemy import not_, func +from typing import List, Annotated +from ..database import get_db +from .. import models, schemas, security, crud + +router = APIRouter( + prefix="/jobs", + dependencies=[Depends(security.verify_access_token)], + responses={401: {"description": "Unauthorized"}}, + tags=['Jobs'] +) + +@router.get("/", response_model=List[schemas.JobGet]) +def get_jobs(db: Session = Depends(get_db)): + jobs = db.query(models.Jobs).all() + + if not jobs: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + response: List[schemas.JobGet] = [] + + for job in jobs: + hardskills = db.query(models.JobsHard_skills.Hard_skillID, models.Hard_skills.Title).\ + join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.JobsHard_skills.JobID == job.JobID).all() + + Hardskills = [hardskill.Title for hardskill in hardskills] + response += [schemas.JobGet(JobID=job.JobID, UserID = job.UserID, Job_name=job.Job_name, Company_name=job.Company_name, + Link_to_job=job.Link_to_job, Year=job.Year, Qualification=job.Qualification, Time=job.Time, + Salary_after_interview=job.Salary_after_interview, Salary=job.Salary, Email=job.Email, Archive=job.Archive, + Responsibilities=job.Responsibilities, Hardskills=Hardskills)] + + return response + +@router.get("/{id}", response_model=schemas.JobGet) +def get_job(id: int, db: Session = Depends(get_db)): + job = db.query(models.Jobs).filter(models.Jobs.JobID == id).first() + + if not job: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + hardskills = db.query(models.JobsHard_skills.Hard_skillID, models.Hard_skills.Title).\ + join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.JobsHard_skills.JobID == id).all() + + Hardskills = [hardskill.Title for hardskill in hardskills] + response = schemas.JobGet(JobID=job.JobID, UserID = job.UserID, Job_name=job.Job_name, Company_name=job.Company_name, + Link_to_job=job.Link_to_job, Year=job.Year, Qualification=job.Qualification, Time=job.Time, + Salary_after_interview=job.Salary_after_interview, Salary=job.Salary, Email=job.Email, Archive=job.Archive, + Responsibilities=job.Responsibilities, Hardskills=Hardskills) + + return response + +@router.get("/matches/{id}", response_model=List[schemas.MatchJob]) +def get_matches(id: int, db: Session = Depends(get_db)): + matches = db.query(models.Matches).filter(models.Matches.JobID == id).order_by(models.Matches.Match.desc()).all() + matches = db.query( + models.Students.StudentID, + models.Students.Name, + models.Students.Type, + models.Students.Faculties, + models.Students.Group, + models.Students.Year, + models.Students.Experience_specialty, + models.Students.Time, + models.Students.Soft_skills, + models.Students.Link, + models.Students.Email, + models.Students.Phone_number, + models.Matches.Match + ).join(models.Matches, models.Students.StudentID == models.Matches.StudentID).\ + filter(models.Matches.JobID == id).\ + order_by(models.Matches.Match.desc()).\ + all() + + if not matches: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return matches + +@router.get("/hardskills/{id}", response_model=List[schemas.JobsHardskill]) +def get_jobs_hardskills(id: int, db: Session = Depends(get_db)): + hardskills = db.query(models.JobsHard_skills.Hard_skillID, models.Hard_skills.Title).\ + join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.JobsHard_skills.JobID == id).all() + + if not hardskills: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return hardskills + +@router.get("/students-search/", response_model=List[schemas.StudentUpdate]) +def get_students( + year: int = None, + time: Annotated[list[str], Query()] = [], + hardskills: Annotated[list[str], Query()] = [], + faculties: str = None, + experience_specialty: bool = None, + db: Session = Depends(get_db) + ): + + if hardskills: + students = db.query( + models.Students.StudentID, + models.Students.Name, + models.Students.Type, + models.Students.Faculties, + models.Students.Group, + models.Students.Уear, + models.Students.Experience_specialty, + models.Students.Time, + models.Students.Soft_skills, + models.Students.Link, + models.Students.Email, + models.Students.Phone_number, + ).join(models.StudentsHard_skills, models.Students.StudentID == models.StudentsHard_skills.StudentID).\ + join(models.Hard_skills, models.StudentsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.Hard_skills.Title.in_(hardskills)).distinct(models.Students.StudentID) + else: + students = db.query(models.Students).filter(models.Students.Experience_specialty == experience_specialty) + + if time: + students = students.filter(models.Students.Time.op("&&")(time)) + + if year: + students = students.filter(models.Students.Year >= year) + + if faculties: + students = students.filter(models.Students.Faculties.ilike("%" + faculties + "%")) + + response: List[schemas.StudentUpdate] = [] + + for student in students: + hardskills = db.query(models.StudentsHard_skills.Hard_skillID, models.Hard_skills.Title).\ + join(models.Hard_skills, models.StudentsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.StudentsHard_skills.StudentID == student.StudentID).all() + + Hardskills = [hardskill.Title for hardskill in hardskills] + response += [schemas.StudentUpdate(Name=student.Name, Type=student.Type, Faculties=student.Faculties, + Group=student.Group, Year=student.Year, Experience_specialty=student.Experience_specialty, + Time=student.Time, Soft_skills=student.Soft_skills, Link=student.Link, Email=student.Email, + Phone_number=student.Phone_number, Hardskills=Hardskills)] + + return response + +@router.get("/responses/{id}", response_model=List[schemas.Response]) +def get_responses(id: int, db: Session = Depends(get_db)): + responses = db.query(models.Responses).filter(models.Responses.JobID == id).all() + + if not responses: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return responses + +@router.get("/responses/", response_model=List[schemas.Response]) +def get_responses_all_unprocessed(db: Session = Depends(get_db)): + responses = db.query( + models.Responses.ResponseID, + models.Responses.StudentID, + models.Responses.JobID, + models.Responses.Status, + models.Responses.Comment, + models.Responses.Link, + ).join(models.Students, models.Students.StudentID == models.Responses.StudentID).\ + filter(models.Responses.Status == "Ожидает рассмотрения").\ + order_by(models.Students.Year.asc()).\ + all() # order_by работате и на буквы в начале группы. Так, сейчас должен работать по идеи + + if not responses: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return responses + +@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.Job) +def create_job( + job: schemas.JobCreate, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + current_user: models.Users = Depends(security.get_current_user) + ): + + new_job = models.Jobs(UserID=current_user.UserID, **job.model_dump(exclude='Hardskills')) + db.add(new_job) + db.flush() + + crud.update_hardskills(new_job.JobID, job.Hardskills, is_job=True, db=db) + + if not new_job.Archive: + crud.update_matches(new_job.JobID, is_job=True, db=db, background_tasks=background_tasks) + + crud.commit_transaction(db) + + return new_job + +@router.put("/{id}", response_model=schemas.Job) +def update_job( + id: int, + updated_job: schemas.JobUpdate, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + current_user: models.Users = Depends(security.get_current_user) + ): + + job_query = db.query(models.Jobs).filter(models.Jobs.JobID == id) + job = job_query.first() + + if not job: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + updated_data = updated_job.model_dump(exclude='Hardskills') + updated_data["UserID"] = current_user.UserID + + job_query.update(updated_data, synchronize_session=False) + db.flush() + + crud.update_hardskills(id, updated_job.Hardskills, is_job=True, db=db) + + if not updated_job.Archive: + crud.update_matches(id, is_job=True, db=db, background_tasks=background_tasks) + else: + db.query(models.Matches).filter(models.Matches.JobID == id).delete() + db.flush() + + crud.commit_transaction(db) + + return job_query.first() + +@router.put("/responses/{id}", status_code=status.HTTP_200_OK) +def update_response(id: int, updated_response: schemas.ResponseUpdate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): + updated_record = crud.update_record(models.Responses, id, updated_response.model_dump(), db, background_tasks) + return updated_record + +@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_job(id: int, db: Session = Depends(get_db)): + job_query = db.query(models.Jobs).filter(models.Jobs.JobID == id) + job = job_query.first() + + if not job: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + job_query.delete() + + crud.commit_transaction(db) + + return Response(status_code=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/mtucijobsbackend/app/routers/service.py b/mtucijobsbackend/app/routers/service.py new file mode 100644 index 0000000..692c5cb --- /dev/null +++ b/mtucijobsbackend/app/routers/service.py @@ -0,0 +1,55 @@ +from fastapi import status, HTTPException, APIRouter, Depends, UploadFile +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from minio import Minio +from typing import List +from io import BytesIO +from os import remove +from starlette.background import BackgroundTask +from ..database import get_db +from ..storage import get_client +from .. import models, schemas, security + +router = APIRouter( + prefix="/services", + dependencies=[Depends(security.verify_api_key)], + tags=['Services'] +) + +@router.get("/resume/{filename}") +def get_resume(filename: str, client: Minio = Depends(get_client)): + response = client.fget_object("tgjobs", filename, filename) + + if not response: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + task = BackgroundTask(remove, filename) + + return FileResponse(path=filename, filename=response.object_name, media_type=response.content_type, background=task) + +@router.get("/hardskills/", response_model=List[schemas.Hard_skill]) +def get_all_hardskills(db: Session = Depends(get_db)): + hardskills = db.query(models.Hard_skills).filter(models.Hard_skills.Title.ilike("%")).all() + + if not hardskills: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return hardskills + +@router.get("/hardskills/{title}", response_model=List[schemas.Hard_skill]) +def get_hardskills(title: str, db: Session = Depends(get_db)): + hardskills = db.query(models.Hard_skills).filter(models.Hard_skills.Title.ilike("%" + title + "%")).all() + + if not hardskills: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return hardskills + +@router.post("/resume/") +async def upload_resume_to_cloud(file: UploadFile, client: Minio = Depends(get_client)): + response = client.put_object("tgjobs", file.filename, BytesIO(await file.read()), file.size, file.content_type) + + if not response: + raise HTTPException(status_code=status.HTTP_409_CONFLICT) + + return {"filename": response.object_name} \ No newline at end of file diff --git a/mtucijobsbackend/app/routers/student.py b/mtucijobsbackend/app/routers/student.py new file mode 100644 index 0000000..0d929de --- /dev/null +++ b/mtucijobsbackend/app/routers/student.py @@ -0,0 +1,197 @@ +from fastapi import status, HTTPException, Response, APIRouter, Depends, Query, BackgroundTasks +from sqlalchemy.orm import Session +from typing import List, Annotated +from ..database import get_db +from .. import models, schemas, security, crud + +router = APIRouter( + prefix="/students", + dependencies=[Depends(security.verify_api_key)], + tags=['Students'] +) + +@router.get("/", response_model=List[schemas.Student]) +def get_students(db: Session = Depends(get_db)): + students = db.query(models.Students) + + return students + +@router.get("/{id}", response_model=schemas.Student) +def get_student(id: int, db: Session = Depends(get_db)): + student = db.query(models.Students).filter(models.Students.StudentID == id).first() + + if not student: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return student + +@router.get("/responses/{id}", response_model=List[schemas.Response]) +def get_responses(id: int, db: Session = Depends(get_db)): + responses = db.query(models.Responses).filter(models.Responses.StudentID == id).all() + + if not responses: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return responses + +@router.get("/matches/{id}", response_model=List[schemas.MatchStudent]) +def get_matches(id: int, db: Session = Depends(get_db)): + matches = db.query( + models.Jobs.JobID, + models.Jobs.Company_name, + models.Jobs.Link_to_job, + models.Jobs.Job_name, + models.Jobs.Year, + models.Jobs.Qualification, + models.Jobs.Time, + models.Jobs.Salary_after_interview, + models.Jobs.Salary, + models.Jobs.Email, + models.Jobs.Responsibilities, + models.Matches.Match + ).join(models.Matches, models.Jobs.JobID == models.Matches.JobID).\ + filter(models.Matches.StudentID == id).\ + order_by(models.Matches.Match.desc()).\ + all() + + if not matches: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return matches + +@router.get("/hardskills/{id}", response_model=List[schemas.StudentsHardskill]) +def get_students_hardskills(id: int, db: Session = Depends(get_db)): + hardskills = db.query(models.StudentsHard_skills.Hard_skillID, models.Hard_skills.Title).\ + join(models.Hard_skills, models.StudentsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.StudentsHard_skills.StudentID == id).all() + + if not hardskills: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + return hardskills + +@router.get("/jobs-search/", response_model=List[schemas.Jobs_search]) +def get_jobs(year: str = None, qualification: bool = None, time: Annotated[list[str], Query()] = [], salary: int = None, hardskills: Annotated[list[str], Query()] = [], search: str = None, db: Session = Depends(get_db)): + + if hardskills: + jobs = db.query( + models.Jobs.Job_name, + models.Jobs.Company_name, + models.Jobs.Link_to_job, + models.Jobs.Year, + models.Jobs.Qualification, + models.Jobs.Time, + models.Jobs.Salary, + models.Jobs.Salary_after_interview, + models.Jobs.Email, + models.Jobs.Responsibilities, + models.Jobs.JobID, + models.JobsHard_skills.JobID, + models.Hard_skills.Title + ).join(models.JobsHard_skills, models.Jobs.JobID == models.JobsHard_skills.JobID).\ + join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\ + filter(models.Hard_skills.Title.in_(hardskills), models.Jobs.Qualification == qualification, models.Jobs.Archive == False).distinct(models.Jobs.JobID) + else: + jobs = db.query(models.Jobs).filter(models.Jobs.Qualification == qualification, models.Jobs.Archive == False) + + if year: + jobs = jobs.filter(models.Jobs.Year >= year) + + if salary: + jobs = jobs.filter(models.Jobs.Salary >= salary) + + if time: + jobs = jobs.filter(models.Jobs.Time.op("&&")(time)) + + if search: + jobs = jobs.filter(models.Jobs.Job_name.match(search)) + + return jobs.all() + +@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.Student) +def create_student(student: schemas.StudentCreate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): + new_student = models.Students(**student.model_dump(exclude='Hardskills')) + db.add(new_student) + db.flush() + + crud.update_hardskills(new_student.StudentID, student.Hardskills, is_job=False, db=db) + crud.update_matches(new_student.StudentID, is_job=False, db=db, background_tasks=background_tasks) + + crud.commit_transaction(db) + + return new_student + +@router.post("/responses/", status_code=status.HTTP_201_CREATED, response_model=schemas.Response) +def add_to_responses(response: schemas.ResponseCreate, db: Session = Depends(get_db)): + new_response = models.Responses(**response.model_dump()) + db.add(new_response) + + crud.commit_transaction(db) + + return new_response + +@router.put("/{id}", response_model=schemas.Student) +def update_student(id: int, updated_student: schemas.StudentUpdate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): + student_query = db.query(models.Students).filter(models.Students.StudentID == id) + student = student_query.first() + + if not student: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + student_query.update(updated_student.model_dump(exclude='Hardskills')) + db.flush() + + crud.update_hardskills(id, updated_student.Hardskills, is_job=False, db=db) + crud.update_matches(id, is_job=False, db=db, background_tasks=background_tasks) + + crud.commit_transaction(db) + + return student_query.first() + +@router.put("/responses/{id}", status_code=status.HTTP_200_OK) +def update_response(id: int, updated_response: schemas.ResponseUpdate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): + updated_record = crud.update_record(models.Responses, id, updated_response.model_dump(), db, background_tasks) + return updated_record + +@router.patch("/{id}", status_code=status.HTTP_200_OK) +def update_students_link(id: int, Link: str, db: Session = Depends(get_db)): + student_query = db.query(models.Students).filter(models.Students.StudentID == id) + student = student_query.first() + + if not student: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + student_query.update({models.Students.Link: Link}) + db.flush() + + crud.commit_transaction(db) + + return student_query.first() + +@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_student(id: int, db: Session = Depends(get_db)): + student_query = db.query(models.Students).filter(models.Students.StudentID == id) + student = student_query.first() + + if not student: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + student_query.delete() + + crud.commit_transaction(db) + + return Response(status_code=status.HTTP_204_NO_CONTENT) + +@router.delete("/responses/{id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_responses(id: int, db: Session = Depends(get_db)): + responses_query = db.query(models.Responses).filter(models.Responses.ResponseID == id) + response = responses_query.first() + + if not response: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + responses_query.delete() + + crud.commit_transaction(db) + + return Response(status_code=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/mtucijobsbackend/app/routers/user.py b/mtucijobsbackend/app/routers/user.py new file mode 100644 index 0000000..2f48202 --- /dev/null +++ b/mtucijobsbackend/app/routers/user.py @@ -0,0 +1,60 @@ +from fastapi import FastAPI, status, HTTPException, Depends, APIRouter +from sqlalchemy.orm import Session +from .. import models, schemas, security, utils +from ..database import get_db + +router = APIRouter( + prefix="/users", + tags=['Users'] +) + +@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + + existing_user = db.query(models.Users).filter(models.Users.Email == user.Email).first() + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User with this email already exists" + ) + + hashed_password = utils.hash(user.Hashed_password) + + user_data = user.model_dump() + user_data["Hashed_password"] = hashed_password + + new_user = models.Users(**user_data) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + return new_user + + +@router.put("/{email}", response_model=schemas.User) +def update_user(email: str, updated_user: schemas.UserUpdate, db: Session = Depends(get_db), current_user: models.Users = Depends(security.get_current_user)): + + if current_user.Email != email: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + existing_user = db.query(models.Users).filter(models.Users.Email == email).first() + if not existing_user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + + if updated_user.Email: + email_user = db.query(models.Users).filter(models.Users.Email == updated_user.Email).first() + if email_user and email_user.Email != email: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already in use") + + if updated_user.Hashed_password: + hashed_password = utils.hash(updated_user.Hashed_password) + updated_user.Hashed_password = hashed_password + + for key, value in updated_user.model_dump(exclude_unset=True).items(): + setattr(existing_user, key, value) + + db.commit() + db.refresh(existing_user) + + return existing_user \ No newline at end of file diff --git a/mtucijobsbackend/app/schemas.py b/mtucijobsbackend/app/schemas.py new file mode 100644 index 0000000..605054c --- /dev/null +++ b/mtucijobsbackend/app/schemas.py @@ -0,0 +1,146 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import List, Optional, Union + +class StudentUpdate(BaseModel): + Name: str + Type: str + Faculties: str + Group: str + Course: Optional[int] + Experience_specialty: bool + Time: List[str] + Soft_skills: str + Link: str + Email: str + Phone_number: str + Hardskills: List[str] + +class Student(BaseModel): + StudentID: int + Name: str + Type: str + Faculties: str + Group: str + Course: Optional[int] + Experience_specialty: bool + Time: List[str] + Soft_skills: str + Link: str + Email: str + Phone_number: str + +class StudentCreate(Student): + Hardskills: List[str] + +class MatchJob(Student): + Match: int + +class Jobs_search(BaseModel): + JobID: int + Company_name: str + Link_to_job: Optional[str] = None + Job_name: str + Year: str + Qualification: bool + Time: List[str] + Salary_after_interview: bool + Salary: int + Email: str + Responsibilities: str + +class MatchStudent(Jobs_search): + Match: int + +class JobBase(BaseModel): + Company_name: str + Link_to_job: Optional[str] = None + Job_name: str + Year: str + Qualification: bool + Salary_after_interview: bool + Salary: int + Email: str + Archive: bool + Responsibilities: str + Time: List[str] + Hardskills: List[str] + +class JobCreate(JobBase): + pass + +class JobUpdate(JobBase): + pass + +class Job(BaseModel): + JobID: Optional[int] + UserID: int + Company_name: Optional[str] + Link_to_job: Optional[str] = None + Job_name: str + Year: str + Qualification: bool + Time: List[str] + Salary_after_interview: bool + Salary: int + Email: str + Archive: bool + Responsibilities: str + +class JobGet(Job): + Hardskills: List[str] + +class ResponseCreate(BaseModel): + StudentID: int + JobID: int + Status: Optional[str] = "Ожидает рассмотрения" + Comment: Optional[str] = None + Link: Optional[str] = None + +class ResponseUpdate(BaseModel): + Status: Optional[str] = "Ожидает рассмотрения" + Comment: Optional[str] = None + Link: Optional[str] = None + +class Response(ResponseCreate): + ResponseID: int + +class Hard_skill(BaseModel): + Hard_skillID: int + Title: str + +class StudentsHardskillCreate(BaseModel): + Title: str + +class StudentsHardskill(BaseModel): + Hard_skillID: int + Title: str + +class JobsHardskillCreate(BaseModel): + Title: str + +class JobsHardskill(BaseModel): + Hard_skillID: int + Title: str + +class UserBase(BaseModel): + Email: Optional[str] + Hashed_password: Optional[str] + +class UserCreate(UserBase): + pass + +class UserUpdate(UserBase): + pass + +class User(BaseModel): + Email: str + +class Token(BaseModel): + access_token: str + refresh_token: Union[str, None] = None + token_type: str = "Bearer" + +class TokenData(BaseModel): + UserID: int + Email: str \ No newline at end of file diff --git a/mtucijobsbackend/app/security.py b/mtucijobsbackend/app/security.py new file mode 100644 index 0000000..e890c00 --- /dev/null +++ b/mtucijobsbackend/app/security.py @@ -0,0 +1,90 @@ +from jose import JWTError, jwt +from datetime import datetime, timedelta +from . import schemas, database, models +from fastapi import Depends, status, HTTPException, Security +from fastapi.security import OAuth2PasswordBearer, APIKeyHeader +from sqlalchemy.orm import Session +from .config import settings + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') +header_scheme = APIKeyHeader(name="X-API-KEY") + +SECRET_KEY = settings.secret_key2 +ALGORITHM = settings.algorithm +ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes +REFRESH_TOKEN_EXPIRE_DAYS = settings.refresh_token_expire_days +X_API_KEY = settings.x_api_key + +def verify_api_key(api_key_header: str = Security(header_scheme)): + if api_key_header == X_API_KEY: + return {"status": "OK"} + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + +def create_access_token(data: dict): + to_encode = data.copy() + + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update( + {"type": "access"}) + to_encode.update( + {"exp": expire}) + + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + return encoded_jwt + +def create_refresh_token(data: dict): + to_encode = data.copy() + + expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) + to_encode.update( + {"type": "refresh"}) + to_encode.update( + {"exp": expire}) + + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + return encoded_jwt + +def decode_and_verify_token(token: str, expected_type: str): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + token_type: str = payload.get("type") + if token_type != expected_type: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Invalid token type: expected {expected_type}, got {token_type}") + except JWTError: + raise credentials_exception + + return payload + + +def verify_access_token(access_token: str = Depends(oauth2_scheme)): + payload = decode_and_verify_token(access_token, expected_type="access") + + user_id: str = payload.get("UserID") + email: str = payload.get("Email") + + token_data = schemas.TokenData(UserID=user_id, Email=email) + + return token_data + +def verify_refresh_token(refresh_token: str): + payload = decode_and_verify_token(refresh_token, expected_type="refresh") + + user_id: str = payload.get("UserID") + + return user_id + + +def get_current_user(token_data: schemas.TokenData = Depends(verify_access_token), db: Session = Depends(database.get_db)): + user = db.query(models.Users).filter(models.Users.UserID == token_data.UserID).first() + if user is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + return user diff --git a/mtucijobsbackend/app/storage.py b/mtucijobsbackend/app/storage.py new file mode 100644 index 0000000..8b00149 --- /dev/null +++ b/mtucijobsbackend/app/storage.py @@ -0,0 +1,21 @@ +from minio import Minio +from minio.error import S3Error +from .config import settings + +def get_client(): + try: + client = Minio( + endpoint=settings.endpoint, + secure=False, + access_key=settings.access_key, + secret_key=settings.secret_key + ) + + found = client.bucket_exists("tgjobs") + + if not found: + client.make_bucket("tgjobs") + + return client + except S3Error as exc: + print("error occurred.", exc) \ No newline at end of file diff --git a/mtucijobsbackend/app/utils.py b/mtucijobsbackend/app/utils.py new file mode 100644 index 0000000..c645ca0 --- /dev/null +++ b/mtucijobsbackend/app/utils.py @@ -0,0 +1,8 @@ +from passlib.context import CryptContext +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def hash(password: str): + return pwd_context.hash(password) + +def verify(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) \ No newline at end of file diff --git a/mtucijobsbackend/requirements.txt b/mtucijobsbackend/requirements.txt new file mode 100644 index 0000000..90dc86f --- /dev/null +++ b/mtucijobsbackend/requirements.txt @@ -0,0 +1,61 @@ +annotated-types==0.7.0 +anyio==4.4.0 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +bcrypt==4.1.3 +certifi==2024.6.2 +cffi==1.16.0 +click==8.1.7 +colorama==0.4.6 +cryptography==42.0.8 +dnspython==2.6.1 +ecdsa==0.19.0 +email_validator==2.1.2 +exceptiongroup==1.2.1 +fastapi==0.111.0 +fastapi-cli==0.0.4 +greenlet==3.0.3 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +idna==3.7 +iniconfig==2.0.0 +Jinja2==3.1.4 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +minio==7.2.7 +orjson==3.10.5 +packaging==24.1 +passlib==1.7.4 +pluggy==1.5.0 +psycopg2-binary==2.9.9 +pyasn1==0.6.0 +pycparser==2.22 +pycryptodome==3.20.0 +pydantic==2.7.4 +pydantic-settings==2.3.3 +pydantic_core==2.18.4 +Pygments==2.18.0 +pytest==8.2.2 +python-dotenv==1.0.1 +python-jose==3.3.0 +python-multipart==0.0.9 +PyYAML==6.0.1 +rich==13.7.1 +rsa==4.9 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +SQLAlchemy==2.0.30 +starlette==0.37.2 +tomli==2.0.1 +typer==0.12.3 +typing_extensions==4.12.2 +ujson==5.10.0 +urllib3==2.2.2 +uvicorn==0.30.1 +watchfiles==0.22.0 +websockets==12.0 +requests==2.32.3 \ No newline at end of file diff --git a/mtucijobsbackend/tests/__init__.py b/mtucijobsbackend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mtucijobsbackend/tests/conftest.py b/mtucijobsbackend/tests/conftest.py new file mode 100644 index 0000000..2579965 --- /dev/null +++ b/mtucijobsbackend/tests/conftest.py @@ -0,0 +1,36 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from app.main import app +from app.config import settings +from app.database import get_db, Base +from app import schemas, models + +# DATABASE_URL = f'postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}:{settings.database_port}/{settings.database_name}_test' +DATABASE_URL = f'postgresql://postgres:2003@localhost:5444/tg_jobs_test' +engine = create_engine(DATABASE_URL) + +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +@pytest.fixture() +def session(): + print("my session fixture ran") + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + +@pytest.fixture() +def client(session): + def override_get_db(): + + try: + yield session + finally: + session.close() + app.dependency_overrides[get_db] = override_get_db + yield TestClient(app) \ No newline at end of file diff --git a/mtucijobsbackend/tests/test_students.py b/mtucijobsbackend/tests/test_students.py new file mode 100644 index 0000000..72cbe97 --- /dev/null +++ b/mtucijobsbackend/tests/test_students.py @@ -0,0 +1,158 @@ +import pytest +from app import schemas, models + +@pytest.fixture() +def create_hardskills(session): + hardskill1 = models.Hard_skills(Title="Python") + hardskill2 = models.Hard_skills(Title="Java") + session.add(hardskill1) + session.add(hardskill2) + session.commit() + session.refresh(hardskill1) + session.refresh(hardskill2) + return [hardskill1, hardskill2] + +Glob_StudentID = 1 # Важно!!! Данная переменная не должна совпадать с StudentID из student_data_test. + +@pytest.fixture +def student_data_test(): + return { + "StudentID": 1234567890, + "Name": "Журавлёв Василий Иванович", + "Type": "Работу", + "Group": "БУТ2101", + "Time": ["20", "30", "40"], + "Soft_skills": "коммуникабельность, работа в команде, адаптивность", + "Link": "https://Vasiliy.com", + "Email": "Vasiliy@gmail.com" + } + +@pytest.fixture +def job_data_test(create_hardskills): + hardskills = create_hardskills + return { + "job": { + "Company_name": "АСУ-ВЭИ", + "Job_name": "Работа с ПЛК", + "Year": "3", + "Qualification": False, + "Soft_skills": "Работа в команде", + "Salary": 25000, + "Email": "info@asu-vei.ru", + "Archive": False, + "Responsibilities": "Разработка методических пособий, для работы с ПЛК. Тестирование scada системы" + }, + "hardskills": [ + { + "JobID": 1, + "Hard_skillID": hardskills[0].Hard_skillID + }, + { + "JobID": 1, + "Hard_skillID": hardskills[1].Hard_skillID + } + ] + } + +@pytest.fixture +def favourite_data_test(create_student, create_job): + student_id = create_student["StudentID"] + return { + "StudentID": student_id, + "JobID": 1 + } + +@pytest.fixture +def create_favourite(client, favourite_data_test): + response = client.post("/students/favourites/", json=favourite_data_test) + assert response.status_code == 201 + return response.json() + +@pytest.fixture +def create_job(client, job_data_test): + response = client.post("/jobs/", json=job_data_test) + assert response.status_code == 201 + return response.json() + +@pytest.fixture +def create_student(client, student_data_test): + response = client.post("/students/", json={"student": student_data_test}) + assert response.status_code == 201 + return response.json() + +def test_root(client): + res = client.get("/") + assert res.json().get('message') == "I'm ok!" + assert res.status_code == 200 + +def test_get_jobs(client): + response = client.get("/students/") + assert response.status_code == 200 + +def test_get_students(client): + response = client.get("/students/") + assert response.status_code == 200 + +def test_get_student_by_id(client, create_student): + student_id = create_student["StudentID"] + response = client.get(f"/students/{student_id}") + assert response.status_code == 200 + student = schemas.Student(**response.json()) + assert student.StudentID == student_id + +# Возможно стоит удалить этот тест +def test_get_student_by_id_not_exist(client, create_student): + response = client.get(f"/students/{Glob_StudentID}") + assert response.status_code == 404 + +def test_create_student(client, student_data_test): + response = client.post("/students/", json={"student": student_data_test}) + assert response.status_code == 201 + new_student = schemas.Student(**response.json()) + assert new_student.Name == "Журавлёв Василий Иванович" + +def test_update_student(client, create_student): + student_id = create_student["StudentID"] + updated_data = { + "Name": "Журавлёв Владимир Иванович", + "Type": "Стажировку", + "Group": "БУТ2101", + "Time": ["20"], + "Soft_skills": "коммуникабельность, адаптивность", + "Link": "https://Vladimir.com", + "Email": "Vladimir@gmail.com" + } + response = client.put(f"/students/{student_id}", json=updated_data) + assert response.status_code == 200 + updated_student = schemas.Student(**response.json()) + assert updated_student.Name == "Журавлёв Владимир Иванович" + assert updated_student.Type == "Стажировку" + assert updated_student.Group == "БУТ2101" + assert updated_student.Time == ["20"] + assert updated_student.Soft_skills == "коммуникабельность, адаптивность" + assert updated_student.Link == "https://Vladimir.com" + assert updated_student.Email == "Vladimir@gmail.com" + +def test_delete_student(client, create_student): + student_id = create_student["StudentID"] + response = client.delete(f"/students/{student_id}") + assert response.status_code == 204 + + response = client.get(f"/students/{student_id}") + assert response.status_code == 404 + +# Для этого теста нужно создать job +def test_add_to_favourites(client, create_favourite): + student_id = create_favourite["StudentID"] + job_id = create_favourite["JobID"] + assert student_id == student_id + assert job_id == 1 + +# Для этого теста наверное надо ещё заполнить таблицу Matches, так что думаю этот тест не очень стабильный, но о работает))) +# def test_get_favourites(client, create_student, create_job, create_favourite): +# student_id = create_student["StudentID"] +# response = client.get(f"/students/favourites/{student_id}") + +# # assert response.status_code == 200 # как это может ломать тест? +# favourites = response.json() +# assert len(favourites) > 0 diff --git a/mtucijobsbot/.dockerignore b/mtucijobsbot/.dockerignore new file mode 100644 index 0000000..600d12f --- /dev/null +++ b/mtucijobsbot/.dockerignore @@ -0,0 +1,13 @@ +**/node_modules +*.md +*.env +Dockerfile +docker-compose.yml +**/.npmignore +**/.dockerignore +**/*.md +**/*.log +**/.vscode +**/.git +**/.eslintrc.json +*.sh diff --git a/mtucijobsbot/.env.development b/mtucijobsbot/.env.development new file mode 100644 index 0000000..c61f583 --- /dev/null +++ b/mtucijobsbot/.env.development @@ -0,0 +1,9 @@ +BOT_TOKEN= +DB_NAME= +DB_HOST= +DB_USER= +DB_PASSWORD= +PORT= +HOOKPORT= +DOMAIN= +WEB_APP= \ No newline at end of file diff --git a/mtucijobsbot/.gitignore b/mtucijobsbot/.gitignore new file mode 100644 index 0000000..08ed734 --- /dev/null +++ b/mtucijobsbot/.gitignore @@ -0,0 +1,4 @@ +.DS_store +**/.DS_Store +node_modules +.env diff --git a/mtucijobsbot/Dockerfile b/mtucijobsbot/Dockerfile new file mode 100644 index 0000000..34f1839 --- /dev/null +++ b/mtucijobsbot/Dockerfile @@ -0,0 +1,12 @@ +FROM node:21.5.0-alpine3.19 + +ENV NEXT_TELEMETRY_DISABLED=1 +WORKDIR /app +COPY package*.json ./ +RUN npm i +COPY . . + +RUN npm run build + +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/mtucijobsbot/build.sh b/mtucijobsbot/build.sh new file mode 100644 index 0000000..01961b1 --- /dev/null +++ b/mtucijobsbot/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +docker build -f Dockerfile . \ + -t mtuci-jobs-image:latest \ + --compress \ + --force-rm diff --git a/mtucijobsbot/deploy.sh b/mtucijobsbot/deploy.sh new file mode 100644 index 0000000..ae23b4a --- /dev/null +++ b/mtucijobsbot/deploy.sh @@ -0,0 +1,5 @@ +#!/bin/bash +git pull +docker-compose -f docker-compose.yml up -d --build +docker exec -t bot-mtuci-jobs ash -c "NODE_ENV=production npx sequelize-cli db:migrate" +docker-compose -f docker-compose.yml logs --tail=10 -f \ No newline at end of file diff --git a/mtucijobsbot/dist/api/resume.js b/mtucijobsbot/dist/api/resume.js new file mode 100644 index 0000000..5d00207 --- /dev/null +++ b/mtucijobsbot/dist/api/resume.js @@ -0,0 +1,40 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +require("dotenv/config"); +const router = express_1.default.Router(); +router.post('/api/resume', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const postData = req.body; + // Проверяем наличие chatId в теле запроса + if (!postData.chatId) { + throw new Error('Chat ID is missing in the request body'); + } + // Ваша логика обработки данных + console.log('Received data:', postData.id); + // Отправка сообщения в боте + // await req.bot.telegram.sendMessage( + // postData.chatId, + // `Received data: ${postData.id}` + // ); + // MessagesService.sendMessage(postData.id, ) + res.status(200).json({ message: 'Data received successfully' }); + } + catch (error) { + console.error(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +})); +exports.default = router; diff --git a/mtucijobsbot/dist/assets/шаблон МТУСИ.docx b/mtucijobsbot/dist/assets/шаблон МТУСИ.docx new file mode 100644 index 0000000000000000000000000000000000000000..cb2b7570c7cd2d7c45c91f2dc754f8ab1034761e GIT binary patch literal 37363 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 B@7DkT literal 0 HcmV?d00001 diff --git a/mtucijobsbot/dist/commands/start.js b/mtucijobsbot/dist/commands/start.js new file mode 100644 index 0000000..dbedec0 --- /dev/null +++ b/mtucijobsbot/dist/commands/start.js @@ -0,0 +1,90 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.startCommand = void 0; +const db_1 = require("../db/db"); +const User_1 = require("../models/User"); +const axios_1 = __importDefault(require("axios")); +function startCommand(ctx) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; + function isBotOwner(userId) { + const ownerUserId = 1053404914; // Замените этот ID на фактический ID владельца + return userId === ownerUserId; + } + try { + yield db_1.sequelize.authenticate(); // Проверка подключения к базе данных + yield db_1.UserBase.sync(); // Создание таблицы, если её нет + console.log('User table has been created or already exists.'); + // Проверка, существует ли пользователь в базе данных + const [userInstance, created] = yield db_1.UserBase.findOrCreate({ + where: { id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id }, + defaults: { + id: ((_b = ctx.from) === null || _b === void 0 ? void 0 : _b.id) || 0, + username: ((_c = ctx.from) === null || _c === void 0 ? void 0 : _c.username) || '', + vacansyindex: 0, + role: isBotOwner((_d = ctx.from) === null || _d === void 0 ? void 0 : _d.id) ? User_1.UserRole.OWNER : User_1.UserRole.CLIENT, + language: ((_e = ctx.from) === null || _e === void 0 ? void 0 : _e.language_code) || 'ru', + message_id: ((_g = (_f = ctx.callbackQuery) === null || _f === void 0 ? void 0 : _f.message) === null || _g === void 0 ? void 0 : _g.message_id) || 0, + last_obj: '', + chat_id: (_h = ctx.chat) === null || _h === void 0 ? void 0 : _h.id, + resume_id: null, + }, + }); + // Выполняем запрос на удаление резюме, если оно существует + try { + const response = yield axios_1.default.delete(`${process.env.API}students/${(_j = ctx.from) === null || _j === void 0 ? void 0 : _j.id}`, { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + }); + if (response.status === 204) { + console.log('Резюме успешно удалено на сервере.'); + yield db_1.Resume.destroy({ where: { id: (_k = ctx.from) === null || _k === void 0 ? void 0 : _k.id } }); // Удаляем запись из базы данных + yield ctx.reply('Ваше резюме было удалено.'); + } + else { + console.warn('Не удалось удалить резюме, сервер вернул статус:', response.status); + yield ctx.reply('Не удалось удалить ваше резюме. Попробуйте позже.'); + } + } + catch (error) { + console.error('Ошибка при удалении резюме:', error); + yield ctx.reply('Произошла ошибка при удалении резюме. Пожалуйста, попробуйте позже.'); + } + // Приветственное сообщение + const greetingMessage = { + text: 'Вас приветствует бот MTUCI jobs! \n\n' + + 'Для подтверждения того, что вы студент МТУСИ, вам потребуется привязать вашу учетную запись LMS к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты.', + buttons: [[{ text: 'Понял', callback: 'accept2' }]], + }; + // Отправка приветственного сообщения с кнопками + yield ctx.reply(greetingMessage.text, { + reply_markup: { + inline_keyboard: greetingMessage.buttons.map(row => row.map(button => ({ + text: button.text, + callback_data: button.callback, + }))), + }, + parse_mode: 'HTML', + }); + console.log('Бот запущен'); + } + catch (e) { + console.error('Произошла ошибка при запуске бота', e); + ctx.reply('Произошла ошибка при запуске. Пожалуйста, попробуйте позже.'); + } + }); +} +exports.startCommand = startCommand; diff --git a/mtucijobsbot/dist/db/db.js b/mtucijobsbot/dist/db/db.js new file mode 100644 index 0000000..546b5fd --- /dev/null +++ b/mtucijobsbot/dist/db/db.js @@ -0,0 +1,109 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Resume = exports.UserBase = exports.sequelize = void 0; +require("dotenv/config"); +const sequelize_1 = require("sequelize"); +const sequelize = new sequelize_1.Sequelize(process.env.DB_NAME || 'custdev', process.env.DB_USER || 'postgres', process.env.DB_PASSWORD, { + host: process.env.DB_HOST, + dialect: 'postgres', +}); +exports.sequelize = sequelize; +class UserBase extends sequelize_1.Model { +} +exports.UserBase = UserBase; +UserBase.init({ + id: { + type: sequelize_1.DataTypes.BIGINT, + primaryKey: true, + }, + username: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + vacansyindex: { + type: sequelize_1.DataTypes.INTEGER, + allowNull: false, + }, + role: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + defaultValue: 'client', + }, + language: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + message_id: { + type: sequelize_1.DataTypes.INTEGER, + allowNull: true, + }, + last_obj: { + type: sequelize_1.DataTypes.STRING, + allowNull: true, + }, + chat_id: { + type: sequelize_1.DataTypes.BIGINT, + allowNull: true, + }, + resume_id: { + type: sequelize_1.DataTypes.BIGINT, + allowNull: true, + references: { + model: 'Resumes', // имя таблицы резюме + key: 'id', + }, + }, +}, { + sequelize, + modelName: 'User', +}); +class Resume extends sequelize_1.Model { +} +exports.Resume = Resume; +Resume.init({ + id: { + type: sequelize_1.DataTypes.BIGINT, + primaryKey: true, + }, + name: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + group: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + type: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + skills: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + softskills: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + email: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, + resumefile: { + type: sequelize_1.DataTypes.STRING, + allowNull: false, + }, +}, { + sequelize, + modelName: 'Resume', +}); +UserBase.belongsTo(Resume, { foreignKey: 'resume_id', as: 'resume' }); +Resume.hasOne(UserBase, { foreignKey: 'resume_id', as: 'user' }); +sequelize + .sync() + .then(() => { + console.log('Model has been synchronized successfully.'); +}) + .catch(error => { + console.error('Error synchronizing model:', error); +}); diff --git a/mtucijobsbot/dist/index.js b/mtucijobsbot/dist/index.js new file mode 100644 index 0000000..23199a1 --- /dev/null +++ b/mtucijobsbot/dist/index.js @@ -0,0 +1,375 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +require("dotenv/config"); +const telegraf_1 = require("telegraf"); +const start_1 = require("./commands/start"); +const menuScenes_1 = require("./modules/menuScenes"); +const express_1 = __importDefault(require("express")); +const cors_1 = __importDefault(require("cors")); +const events_1 = require("./modules/scenes/events"); +const form_data_1 = __importDefault(require("form-data")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const axios_1 = __importDefault(require("axios")); +const savevacansy_1 = require("./modules/scenes/savevacansy"); +const accept_1 = require("./modules/scenes/accept"); +const app = (0, express_1.default)(); +const bot = new telegraf_1.Telegraf(process.env.BOT_TOKEN); +app.use((req, res, next) => { + req.bot = bot; + next(); +}); +app.use(express_1.default.json()); +app.use((0, cors_1.default)()); +bot.start(start_1.startCommand); +app.post('/api/resume', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const postData = req.body; + const resumeTemplatePath = path_1.default.resolve(__dirname, 'assets', 'шаблон МТУСИ.docx'); + yield bot.telegram.sendDocument(postData.id, { source: resumeTemplatePath }, { caption: 'Пример резюме' }); + yield bot.telegram.sendMessage(postData.id, `Ваша анкета заполнена! Отправьте своё резюме для завершения регистрации`, { + reply_markup: { + inline_keyboard: [ + [ + { text: 'Загрузить резюме', callback_data: 'uploadresume' }, + // { text: 'Пропустить загрузку', callback_data: 'skip' }, + ], + ], + }, + }); + console.log(`Ваш id ${postData.id}`); + return res.json({ status: 'success' }); + } + catch (e) { + console.error('Error:', e); + res.status(500).json({ error: 'An error occurred' }); + throw e; + } +})); +// const resumeScene: Scenes.WizardScene = new Scenes.WizardScene( +// 'resumeScene', +// async ctx => { +// await ctx.reply( +// 'Отправьте своё резюме в PDF формате или отправьте /cancel для отмены.' +// ); +// return ctx.wizard.next(); +// }, +// async ctx => { +// if ( +// ctx.message && +// 'text' in ctx.message && +// ctx.from && +// ctx.message.text == '/cancel' +// ) { +// await ctx.reply('Отправка резюме отменена.'); +// await ctx.reply( +// 'Меню', +// Markup.keyboard([ +// ['Моя анкета'], +// ['Вакансии'], +// ['Уведомления: включены'], +// ]).resize() +// ); +// return ctx.scene.leave(); +// } +// if (ctx.message && 'document' in ctx.message && ctx.from) { +// const file = ctx.message.document; +// if (file.mime_type !== 'application/pdf') { +// ctx.reply('Пожалуйста, отправьте файл в формате PDF.'); +// return; +// } +// try { +// const fileLink = await ctx.telegram.getFileLink(file.file_id); +// const filePath = path.join(__dirname, `${file.file_id}.pdf`); +// // Загрузка файла на локальную машину +// const response = await axios.get(fileLink.href, { +// responseType: 'stream', +// }); +// response.data +// .pipe(fs.createWriteStream(filePath)) +// .on('finish', async () => { +// // Создание формы данных +// const form = new FormData(); +// form.append('file', fs.createReadStream(filePath)); +// try { +// // Установка времени ожидания (в миллисекундах) +// const uploadResponse = await axios.post( +// `${process.env.API}services/resume/`, +// form, +// { +// headers: { +// ...form.getHeaders(), +// 'X-API-KEY': +// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', +// }, +// timeout: 10000, // Увеличьте время ожидания до 10 секунд +// } +// ); +// const fileName = uploadResponse.data.filename; +// // Выполнение PUT-запроса для обновления поля Link у студента +// await axios.patch( +// `${process.env.API}students/${ +// ctx.from?.id +// }?Link=${encodeURIComponent(fileName)}`, +// {}, +// { +// headers: { +// 'X-API-KEY': +// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', +// 'Content-Type': 'application/json', +// }, +// } +// ); +// await ctx.reply('Резюме успешно загружено.'); +// await ctx.reply( +// 'Меню', +// Markup.keyboard([ +// ['Моя анкета'], +// ['Вакансии'], +// ['Уведомления: включены'], +// ]).resize() +// ); +// return ctx.scene.leave(); +// } catch (uploadError) { +// console.error('Ошибка при загрузке резюме на API:', uploadError); +// await ctx.reply('Произошла ошибка при загрузке резюме.'); +// return ctx.scene.leave(); +// } finally { +// // Удаление временного файла после загрузки +// fs.unlinkSync(filePath); +// } +// }); +// } catch (error) { +// console.error('Ошибка при загрузке файла:', error); +// await ctx.reply('Произошла ошибка при загрузке файла.'); +// } +// } else { +// await ctx.reply('Отправьте файл в формате PDF'); +// } +// } +// ); +const resumeScene = new telegraf_1.Scenes.WizardScene('resumeScene', (ctx) => __awaiter(void 0, void 0, void 0, function* () { + yield ctx.reply('Отправьте своё резюме в формате Word (DOC/DOCX) или отправьте /cancel для отмены.'); + return ctx.wizard.next(); +}), (ctx) => __awaiter(void 0, void 0, void 0, function* () { + if (ctx.message && + 'text' in ctx.message && + ctx.from && + ctx.message.text === '/cancel') { + yield ctx.reply('Отправка резюме отменена.'); + yield ctx.reply('Меню', telegraf_1.Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + ['Уведомления: включены'], + ]).resize()); + return ctx.scene.leave(); + } + if (ctx.message && 'document' in ctx.message && ctx.from) { + const file = ctx.message.document; + const allowedMimeTypes = [ + // 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]; + if (!allowedMimeTypes.includes(file.mime_type || '')) { + yield ctx.reply('Пожалуйста, отправьте файл в формате DOC или DOCX.'); + return; + } + try { + const fileLink = yield ctx.telegram.getFileLink(file.file_id); + const fileExtension = file.mime_type === 'application/pdf' + ? 'pdf' + : file.mime_type === 'application/msword' + ? 'doc' + : 'docx'; + const filePath = path_1.default.join(__dirname, `${file.file_id}.${fileExtension}`); + // Загрузка файла на локальную машину + const response = yield axios_1.default.get(fileLink.href, { + responseType: 'stream', + }); + response.data + .pipe(fs_1.default.createWriteStream(filePath)) + .on('finish', () => __awaiter(void 0, void 0, void 0, function* () { + var _a; + const form = new form_data_1.default(); + form.append('file', fs_1.default.createReadStream(filePath)); + try { + const uploadResponse = yield axios_1.default.post(`${process.env.API}services/resume/`, form, { + headers: Object.assign(Object.assign({}, form.getHeaders()), { 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n' }), + timeout: 10000, + }); + const fileName = uploadResponse.data.filename; + yield axios_1.default.patch(`${process.env.API}students/${(_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id}?Link=${encodeURIComponent(fileName)}`, {}, { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + 'Content-Type': 'application/json', + }, + }); + yield ctx.reply('Резюме успешно загружено.'); + yield ctx.reply('Меню', telegraf_1.Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + ['Уведомления: включены'], + ]).resize()); + return ctx.scene.leave(); + } + catch (uploadError) { + console.error('Ошибка при загрузке резюме на API:', uploadError); + yield ctx.reply('Произошла ошибка при загрузке резюме.'); + return ctx.scene.leave(); + } + finally { + fs_1.default.unlinkSync(filePath); + } + })); + } + catch (error) { + console.error('Ошибка при загрузке файла:', error); + yield ctx.reply('Произошла ошибка при загрузке файла.'); + } + } + else { + yield ctx.reply('Пожалуйста, отправьте файл в формате PDF, DOC или DOCX.'); + } +})); +// app.post('/api/resume', async (req: Request, res: Response) => { +// try { +// const postData = req.body; +// // Проверяем наличие chatId в теле запроса +// if (!postData.StudentID) { +// throw new Error('Chat ID is missing in the request body'); +// } +// const [userInstance, created] = await Resume.findOrCreate({ +// where: { id: postData.StudentID }, +// defaults: { +// id: postData.StudentID || 0, +// name: postData.Name, +// group: postData.Group, +// type: postData.Type, +// skills: '', +// softskills: postData.Soft_skills, +// email: postData.Email, +// }, +// }); +// await UserBase.update( +// { resume_id: postData.StudentID }, +// { where: { id: postData.StudentID } } +// ); +// if (userInstance instanceof Resume) { +// // userInstance теперь имеет тип User +// if (created) { +// console.log('New user created:', userInstance); +// } else { +// console.log('User already exists:', userInstance); +// } +// console.log('Привет! Вы успешно зарегистрированы.'); +// } else { +// console.error('Ошибка: userInstance не является экземпляром User.'); +// } +// console.log('Received data:', postData); +// res.status(200).json({ message: 'Data received successfully' }); +// } catch (error) { +// console.error(error); +// res.status(500).json({ error: 'Internal Server Error' }); +// } +// }); +app.post('/webhook', (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const postData = req.body; + const entityType = postData.data.entity_type; + const updatedMatches = postData.data.updated_matches; + console.log(postData); + if (entityType === 'job') { + for (const match of updatedMatches) { + const chatId = match.student_id; + const jobId = match.entity_id; // Получаем ID вакансии + const messageText = 'Ваше совпадение с вакансией было обновлено. Вот детали вакансии:'; + if (!chatId) { + throw new Error('Неправильный Chat ID (student_id) в теле запроса.'); + } + // Выполняем GET-запрос для получения данных о вакансии + const response = yield axios_1.default.get(`${process.env.API}jobs/${jobId}`, // Запрашиваем данные о вакансии по её ID + { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + }); + const vacancyData = response.data; // Данные о вакансии + console.log(vacancyData); + // Отправляем вакансию пользователю + yield sendVacancyToUser(chatId, vacancyData); + console.log(`Сообщение с вакансией отправлено пользователю с ID ${chatId}`); + } + } + return res.status(200).json({ status: 'success' }); + } + catch (e) { + console.error('Error:', e); + res.status(500).json({ error: 'Произошла ошибка при отправке сообщения' }); + } +})); +// Функция для отправки вакансии пользователю +function sendVacancyToUser(chatId, data) { + return __awaiter(this, void 0, void 0, function* () { + yield bot.telegram.sendMessage(chatId, `${data.Job_name}\n\n` + + `Компания: ${data.Company_name}\n` + + `Заработная плата: ${data.Salary} руб/мес\n` + + `Контактные данные: ${data.Email}\n\n` + + `Требования к кандидату:\n` + + ` - ${data.Year} курс\n` + + ` - Опыт работы по специальности: ${data.Qualification}\n` + + ` - Soft skills: ${data.Soft_skills}\n` + + `Обязанности:\n` + + `${data.Responsibilities}`, { + parse_mode: 'HTML', // Используем HTML для форматирования текста + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Сохранить вакансию', + callback_data: `savevacancy+${data.JobID}`, + }, + ], + ], + }, + }); + }); +} +// Обработчик для получения файла резюме +const stage = new telegraf_1.Scenes.Stage([resumeScene]); +bot.use((0, telegraf_1.session)()); +bot.use(stage.middleware()); +bot.use(menuScenes_1.menuSceneMiddleware); +(0, events_1.events)(bot); +(0, accept_1.accept)(bot); +bot.action('uploadresume', (ctx) => __awaiter(void 0, void 0, void 0, function* () { + ctx.scene.enter('resumeScene'); + ctx.answerCbQuery(); +})); +(0, savevacansy_1.saveVacansy)(bot); +bot + .launch({ + webhook: { + domain: process.env.DOMAIN || '', + port: parseInt(process.env.HOOKPORT || ''), + }, +}) + .then(() => console.log('Webhook bot listening on port', parseInt(process.env.HOOKPORT || ''))); +// bot.launch().then(() => { +// console.log('Бот запущен'); +// }); +app.listen(process.env.PORT, () => { + console.log(`Server is running at http://localhost:${process.env.PORT}`); +}); diff --git a/mtucijobsbot/dist/languages/en.json b/mtucijobsbot/dist/languages/en.json new file mode 100644 index 0000000..88a8207 --- /dev/null +++ b/mtucijobsbot/dist/languages/en.json @@ -0,0 +1,24 @@ +{ + "greeting": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept" }, + { "text": "Понял, не запрашивать данные", "callback": "accept" } + ] + ] + }, + "greeting2": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept2" }, + { "text": "Понял, не запрашивать данные", "callback": "accept2" } + ] + ] + } +} diff --git a/mtucijobsbot/dist/languages/ru.json b/mtucijobsbot/dist/languages/ru.json new file mode 100644 index 0000000..ec84b86 --- /dev/null +++ b/mtucijobsbot/dist/languages/ru.json @@ -0,0 +1,24 @@ +{ + "greeting": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept" }, + { "text": "Понял, не запрашивать данные", "callback": "accept" } + ] + ] + }, + "greeting2": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept2" }, + { "text": "Понял, не запрашивать данные", "callback": "accept2" } + ] + ] + } +} diff --git a/mtucijobsbot/dist/models/User.js b/mtucijobsbot/dist/models/User.js new file mode 100644 index 0000000..5711c21 --- /dev/null +++ b/mtucijobsbot/dist/models/User.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UserRole = void 0; +// Перечисление ролей пользователя +var UserRole; +(function (UserRole) { + UserRole["OWNER"] = "owner"; + UserRole["ADMIN"] = "admin"; + UserRole["MODERATOR"] = "moderator"; + UserRole["CLIENT"] = "client"; +})(UserRole || (exports.UserRole = UserRole = {})); diff --git a/mtucijobsbot/dist/modules/menuController.js b/mtucijobsbot/dist/modules/menuController.js new file mode 100644 index 0000000..2c0cecf --- /dev/null +++ b/mtucijobsbot/dist/modules/menuController.js @@ -0,0 +1,50 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MenuController = void 0; +const db_1 = require("../db/db"); +class MenuController { + static showLanguageMenu(ctx) { + return __awaiter(this, void 0, void 0, function* () { + // await MessagesService.sendMessage(ctx, 'greeting'); + }); + } + static setLanguage(ctx) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + try { + if (!ctx.callbackQuery) { + throw new Error('CallbackQuery is not defined.'); + } + const languageCode = ctx.callbackQuery && 'data' in ctx.callbackQuery + ? ctx.callbackQuery.data.split('_')[1] + : undefined; + if (languageCode) { + // Обновляем язык пользователя в базе данных + yield db_1.UserBase.update({ language: languageCode }, { where: { id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id } }); + // Обновляем язык в объекте контекста + if (ctx.from) { + ctx.from.language_code = languageCode; + } + } + else { + // Если data отсутствует в callbackQuery, обработка не может быть выполнена + throw new Error('Missing data property in callbackQuery.'); + } + } + catch (error) { + console.error('Error during setLanguage:', error); + console.log('Произошла ошибка при обновлении языка. Пожалуйста, попробуйте снова позже.'); + } + }); + } +} +exports.MenuController = MenuController; diff --git a/mtucijobsbot/dist/modules/menuScenes.js b/mtucijobsbot/dist/modules/menuScenes.js new file mode 100644 index 0000000..8a4d544 --- /dev/null +++ b/mtucijobsbot/dist/modules/menuScenes.js @@ -0,0 +1,38 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.menuSceneMiddleware = void 0; +const db_1 = require("../db/db"); +const messagesService_1 = require("../services/messagesService"); +// Middleware для обработки callback'ов +const callbackQueryMiddleware = (ctx, next) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + try { + const languageCode = ctx.callbackQuery && 'data' in ctx.callbackQuery + ? ctx.callbackQuery.data.split('_')[1] + : undefined; + // Обновляем язык пользователя в базе данных + if (languageCode && ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id)) { + yield db_1.UserBase.update({ language: languageCode }, { where: { id: ctx.from.id } }); + // Отправляем сообщение об успешном обновлении языка + console.log(`Язык обновлен: ${languageCode}`); + yield ctx.answerCbQuery(); + yield messagesService_1.MessagesService.sendMessage(ctx, 'greeting'); + } + } + catch (error) { + console.error('Error during callback handling:', error); + console.log("Произошла ошибка при обработке callback'а. Пожалуйста, попробуйте снова позже."); + } + // Вызываем следующий middleware + return next(); +}); +exports.menuSceneMiddleware = callbackQueryMiddleware; diff --git a/mtucijobsbot/dist/modules/scenes/accept.js b/mtucijobsbot/dist/modules/scenes/accept.js new file mode 100644 index 0000000..9e8bde6 --- /dev/null +++ b/mtucijobsbot/dist/modules/scenes/accept.js @@ -0,0 +1,112 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.accept = void 0; +const axios_1 = __importDefault(require("axios")); +require("dotenv/config"); +// const API_BASE_URL = 'https://mtucitech.ru/api/external/mtuci_jobs'; +// const BEARER_TOKEN = 'zKbgaXQv{pvGtQm~U9$urtD#QsiHc@Ie'; +function accept(bot) { + bot.action('accept', (ctx) => __awaiter(this, void 0, void 0, function* () { + var _a; + try { + const response = yield axios_1.default.get(`${process.env.MTUCI_TECH}/check?telegram_id=${ctx.from.id}`, { + headers: { + Authorization: `Bearer ${process.env.BEARER_TOKEN}`, + }, + }); + const data = response.data; + // Логика обработки ответа + switch (data.status) { + case 338: + ctx.answerCbQuery('Ошибка на стороне сервера. Пожалуйста, сообщите нам.'); + break; + case 339: + ctx.answerCbQuery('Пользователь не привязан к проекту.'); + break; + case 340: + ctx.reply('Можете заполнить своё резюме', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Резюме', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + ctx.answerCbQuery('Пользователь найден, но не привязан LMS.'); + break; + case 341: + ctx.reply('Можете заполнить своё резюме', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Резюме', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + ctx.answerCbQuery('Пользователь найден, LMS привязан. Можно запросить дополнительные данные.'); + default: + ctx.answerCbQuery('Неизвестный статус.'); + break; + } + } + catch (error) { + if (axios_1.default.isAxiosError(error)) { + // Обработка ошибок Axios + if (error.response && error.response.status === 409) { + ctx.answerCbQuery('Вакансия уже добавлена в избранное'); + } + else { + ctx.answerCbQuery('Произошла ошибка'); + console.error(((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error.message); + } + } + else { + // Обработка других типов ошибок + ctx.answerCbQuery('Произошла неизвестная ошибка'); + console.error(error); + } + } + })); + bot.action('accept2', (ctx) => __awaiter(this, void 0, void 0, function* () { + ctx.reply('Можете заполнить своё резюме', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Резюме', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + ctx.answerCbQuery(); + })); +} +exports.accept = accept; diff --git a/mtucijobsbot/dist/modules/scenes/events.js b/mtucijobsbot/dist/modules/scenes/events.js new file mode 100644 index 0000000..76b46ef --- /dev/null +++ b/mtucijobsbot/dist/modules/scenes/events.js @@ -0,0 +1,292 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.events = void 0; +const telegraf_1 = require("telegraf"); +const db_1 = require("../../db/db"); +const axios_1 = __importDefault(require("axios")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +let vacancies = []; +function events(bot) { + bot.action('skip', (ctx) => __awaiter(this, void 0, void 0, function* () { + ctx.reply('Меню', telegraf_1.Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + // ['Уведомления: включены'], + ]).resize()); + ctx.answerCbQuery(); + })); + bot.hears('Вернуться в меню', (ctx) => __awaiter(this, void 0, void 0, function* () { + ctx.reply('Меню', telegraf_1.Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + // ['Уведомления: включены'], + ]).resize()); + })); + bot.hears('Моя анкета', (ctx) => __awaiter(this, void 0, void 0, function* () { + try { + // Выполнение первого GET-запроса для получения данных пользователя + const response = yield axios_1.default.get(`${process.env.API}students/${ctx.from.id}`, { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + }); + const data = response.data; + const resumeFile = yield db_1.Resume.findOne({ where: { id: ctx.from.id } }); + // Проверяем, существует ли резюме + if (resumeFile && resumeFile.resumefile.length > 1) { + // Выполнение второго GET-запроса для получения файла с резюме + const resumeResponse = yield (0, axios_1.default)({ + method: 'GET', + url: `${process.env.API}services/resume/${resumeFile.resumefile}`, + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + responseType: 'stream', + timeout: 10000, + }); + // Сохраняем файл временно на диск + const resumePath = path_1.default.join(__dirname, 'resume.pdf'); + const writer = fs_1.default.createWriteStream(resumePath); + resumeResponse.data.pipe(writer); + writer.on('finish', () => __awaiter(this, void 0, void 0, function* () { + // Отправка файла и сообщения с данными из API + yield ctx.replyWithDocument({ source: resumePath }); + yield ctx.replyWithHTML(`Имя: ${data.Name}\nГруппа: ${data.Group}\nТип: ${data.Type}\nГотов на занятость: ${data.Time.join()}\nО себе: ${data.Soft_skills}\nПочта: ${data.Email}\n`, { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Редактировать анкету', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + [ + { + text: 'Обновить резюме', + callback_data: 'update_resume', + }, + ], + ], + }, + }); + // Удаляем временный файл + fs_1.default.unlinkSync(resumePath); + })); + writer.on('error', err => { + console.error('Ошибка при записи файла резюме:', err); + ctx.reply('Произошла ошибка при получении файла резюме.'); + }); + } + else { + // Отправка только сообщения с данными из API, если резюме не существует + yield ctx.replyWithHTML(`Имя: ${data.Name}\nГруппа: ${data.Group}\nТип: ${data.Type}\nГотов на занятость: ${data.Time.join()}\nО себе: ${data.Soft_skills}\nПочта: ${data.Email}\n`, { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Редактировать', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + [ + { + text: 'Загрузить резюме', + callback_data: 'update_resume', + }, + ], + ], + }, + }); + } + } + catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + })); + bot.hears('Вакансии', (ctx) => __awaiter(this, void 0, void 0, function* () { + try { + yield ctx.reply('Выберите тип вакансии:', telegraf_1.Markup.keyboard([ + ['Актуальные вакансии'], + ['Сохраненные вакансии'], + ['Поиск'], + ['Вернуться в меню'], + ]).resize()); + } + catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + })); + bot.hears('Поиск', (ctx) => __awaiter(this, void 0, void 0, function* () { + try { + yield ctx.reply('Поиск вакансий:', telegraf_1.Markup.keyboard([ + ['Актуальные вакансии'], + ['Сохраненные вакансии'], + ['Вернуться в меню'], + ]).resize()); + ctx.reply('Страница с поиском вакансий', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Перейти', + web_app: { url: process.env.WEB_APP_SEARCH || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + } + catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + })); + bot.hears('Актуальные вакансии', (ctx) => __awaiter(this, void 0, void 0, function* () { + try { + yield db_1.UserBase.update({ vacansyindex: 0 }, { + where: { + id: ctx.from.id, + }, + }); + // Выполнение первого GET-запроса для получения данных пользователя + const response = yield axios_1.default.get(`${process.env.API}students/matches/${ctx.from.id}`, { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + }); + vacancies = response.data; + console.log(vacancies); + if (vacancies.length === 0) { + ctx.reply('На данный момент нет актуальных вакансий.'); + return; + } + let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = user === null || user === void 0 ? void 0 : user.vacansyindex; + yield sendVacancy(ctx, currentIndex, true); + } + } + catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + })); + bot.hears('Сохраненные вакансии', (ctx) => __awaiter(this, void 0, void 0, function* () { + try { + yield db_1.UserBase.update({ vacansyindex: 0 }, { + where: { + id: ctx.from.id, + }, + }); + // Выполнение первого GET-запроса для получения данных пользователя + const response = yield axios_1.default.get(`${process.env.API}students/favourites/${ctx.from.id}`, { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + }); + vacancies = response.data; + console.log(vacancies); + if (vacancies.length === 0) { + ctx.reply('На данный момент нет сохранённых вакансий.'); + return; + } + let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = user === null || user === void 0 ? void 0 : user.vacansyindex; + yield sendVacancy(ctx, currentIndex, false); + } + } + catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('На данный момент нет сохранённых вакансий.'); + // ctx.reply('Произошла ошибка при получении данных из API.'); + } + })); + bot.action('next', (ctx) => __awaiter(this, void 0, void 0, function* () { + let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = (user === null || user === void 0 ? void 0 : user.vacansyindex) + 1; + if (currentIndex <= vacancies.length - 1) { + yield db_1.UserBase.update({ vacansyindex: currentIndex }, { where: { id: ctx.from.id } }); + yield sendVacancy(ctx, currentIndex, true); + ctx.answerCbQuery(); + } + else { + ctx.answerCbQuery('Это последняя вакансия.'); + } + } + })); + bot.action('update_resume', (ctx) => __awaiter(this, void 0, void 0, function* () { + yield ctx.scene.enter('resumeScene'); + yield ctx.answerCbQuery(); + })); + bot.action('prev', (ctx) => __awaiter(this, void 0, void 0, function* () { + let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = (user === null || user === void 0 ? void 0 : user.vacansyindex) - 1; + if (currentIndex >= 0) { + yield db_1.UserBase.update({ vacansyindex: currentIndex }, { where: { id: ctx.from.id } }); + yield sendVacancy(ctx, currentIndex, true); + ctx.answerCbQuery(); + } + else { + ctx.answerCbQuery('Это первая вакансия.'); + } + } + })); + function sendVacancy(ctx, index, showSaveButton) { + return __awaiter(this, void 0, void 0, function* () { + const data = vacancies[index]; + // Формируем кнопки навигации + let inlineKeyboard = [ + [ + { text: 'Назад', callback_data: 'prev' }, + { text: `${index + 1}/${vacancies.length}`, callback_data: 'new' }, + { text: 'Далее', callback_data: 'next' }, + ], + ]; + // Добавляем кнопку "Сохранить вакансию" только если showSaveButton = true + if (showSaveButton) { + inlineKeyboard.push([ + { + text: 'Сохранить вакансию', + callback_data: `savevacancy+${data.JobID}`, + }, + ]); + } + // Отправляем сообщение с вакансиями + yield ctx.replyWithHTML(`${data.Job_name}\n\n` + + `Компания: ${data.Company_name}\n` + + `Заработная плата: ${data.Salary} руб/мес\n` + + `Контактные данные: ${data.Email}\n\n` + + `Требования к кандидату:\n` + + ` - ${data.Year} курс\n` + + ` - Опыт работы по специальности: ${data.Qualification}\n` + + ` - Soft skills: ${data.Soft_skills}\n` + + `Обязанности:\n` + + `${data.Responsibilities}`, { + reply_markup: { + inline_keyboard: inlineKeyboard, + }, + }); + }); + } +} +exports.events = events; diff --git a/mtucijobsbot/dist/modules/scenes/invest.js b/mtucijobsbot/dist/modules/scenes/invest.js new file mode 100644 index 0000000..1ec8821 --- /dev/null +++ b/mtucijobsbot/dist/modules/scenes/invest.js @@ -0,0 +1,39 @@ +"use strict"; +// import { Telegraf, Context, Scenes } from 'telegraf'; +// import { MessagesService } from '../../services/messagesService'; +// import fs from 'fs'; +// import fetch from 'node-fetch'; +// import { MyWizardContext } from '../..'; +// import { updateUrls } from '../../db/db'; +// export function invest(bot: Telegraf) { +// bot.action('invest', async ctx => { +// await MessagesService.sendMessage(ctx, 'invest'); +// await ctx.answerCbQuery(); +// }); +// bot.action('investscreen', async ctx => { +// await ctx.scene.enter('investHandler'); +// await ctx.answerCbQuery(); +// }); +// } +// export const investHandler: Scenes.WizardScene = +// new Scenes.WizardScene( +// 'investHandler', +// async ctx => { +// await MessagesService.sendMessage(ctx, 'getpoint'); +// return ctx.wizard.next(); +// }, +// async ctx => { +// if (ctx.message && 'text' in ctx.message && ctx.from) { +// const userUrls = ctx.message.text; +// await updateUrls(ctx.from.id, 'invest', userUrls, true); +// await MessagesService.sendMessage(ctx, 'thanks'); +// await MessagesService.sendMessage(ctx, 'invest'); +// return ctx.scene.leave(); +// } else { +// console.error( +// 'Ошибка: отсутствует текстовое сообщение или свойство from в контексте.' +// ); +// return ctx.scene.leave(); +// } +// } +// ); diff --git a/mtucijobsbot/dist/modules/scenes/savevacansy.js b/mtucijobsbot/dist/modules/scenes/savevacansy.js new file mode 100644 index 0000000..b102cd4 --- /dev/null +++ b/mtucijobsbot/dist/modules/scenes/savevacansy.js @@ -0,0 +1,55 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.saveVacansy = void 0; +const axios_1 = __importDefault(require("axios")); +require("dotenv/config"); +function saveVacansy(bot) { + bot.on('callback_query', (ctx) => __awaiter(this, void 0, void 0, function* () { + var _a; + if (ctx.callbackQuery && 'data' in ctx.callbackQuery) { + const callbackData = ctx.callbackQuery.data; + if (callbackData.startsWith('savevacancy+')) { + const jobID = callbackData.split('+')[1]; + try { + const data = yield axios_1.default.post(`${process.env.API}students/favourites/`, { StudentID: ctx.from.id, JobID: jobID }, { + headers: { + 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + }); + console.log(data); + ctx.answerCbQuery('Вакансия была сохранена'); + } + catch (error) { + if (axios_1.default.isAxiosError(error)) { + // Обработка ошибок Axios + if (error.response && error.response.status === 409) { + ctx.answerCbQuery('Вакансия уже добавлена в избранное'); + } + else { + ctx.answerCbQuery('Произошла ошибка при сохранении вакансии'); + console.error(((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error.message); + } + } + else { + // Обработка других типов ошибок + ctx.answerCbQuery('Произошла неизвестная ошибка'); + console.error(error); + } + } + } + } + })); +} +exports.saveVacansy = saveVacansy; diff --git a/mtucijobsbot/dist/services/messagesService.js b/mtucijobsbot/dist/services/messagesService.js new file mode 100644 index 0000000..533e45b --- /dev/null +++ b/mtucijobsbot/dist/services/messagesService.js @@ -0,0 +1,277 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MessagesService = void 0; +// Импортируем необходимые модули +const fs = __importStar(require("fs")); +// import { CustomContext } from '../types/telegram'; +const path_1 = __importDefault(require("path")); +const db_1 = require("../db/db"); +const telegraf_1 = require("telegraf"); +class MessagesService { + static loadMessages(lang) { + if (!this.messages[lang]) { + const filePath = path_1.default.join(__dirname, `../languages/${lang}.json`); + const data = fs.readFileSync(filePath, 'utf-8'); + this.messages[lang] = JSON.parse(data); + } + return this.messages[lang]; + } + static backMessage(ctx) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c; + try { + const user = (yield db_1.UserBase.findOne({ + where: { + id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id, + }, + })); + if (user !== null && ctx.chat) { + yield ctx.telegram.copyMessage((_b = ctx.chat) === null || _b === void 0 ? void 0 : _b.id, (_c = ctx.chat) === null || _c === void 0 ? void 0 : _c.id, user.message_id - 1); + } + else { + console.log('error'); + } + } + catch (error) { + console.log(error); + } + }); + } + static hiddenMessage(ctx, key) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c, _d, _e, _f; + try { + const user = (yield db_1.UserBase.findOne({ + where: { + id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id, + }, + })); + if (user && user.message_id && ((_b = ctx.callbackQuery) === null || _b === void 0 ? void 0 : _b.message)) { + yield ctx.telegram.deleteMessage(((_c = ctx.chat) === null || _c === void 0 ? void 0 : _c.id) || 0, (_e = (_d = ctx.callbackQuery) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.message_id); + console.log('Hidden message deleted successfully.'); + yield db_1.UserBase.update({ last_obj: key }, // Новое значение + { + where: { + id: (_f = ctx.from) === null || _f === void 0 ? void 0 : _f.id, // Условие для выбора записей + }, + }); + } + else { + console.log('User or message not found.'); + } + } + catch (error) { + console.error('Error deleting last message:', error); + ctx.reply('An error occurred while deleting the last message. Please try again later.'); + } + }); + } + static deleteLastMessage(ctx) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c, _d, _e, _f, _g, _h; + try { + const user = (yield db_1.UserBase.findOne({ + where: { + id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id, + }, + })); + if (user && user.message_id && ((_b = ctx.callbackQuery) === null || _b === void 0 ? void 0 : _b.message)) { + yield ctx.telegram.deleteMessage(((_c = ctx.chat) === null || _c === void 0 ? void 0 : _c.id) || 0, (_e = (_d = ctx.callbackQuery) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.message_id); + console.log('Last message deleted successfully.'); + yield db_1.UserBase.update({ message_id: (_g = (_f = ctx.callbackQuery) === null || _f === void 0 ? void 0 : _f.message) === null || _g === void 0 ? void 0 : _g.message_id }, // Новое значение + { + where: { + id: (_h = ctx.from) === null || _h === void 0 ? void 0 : _h.id, // Условие для выбора записей + }, + }); + } + else { + console.log('User or message not found.'); + } + } + catch (error) { + console.error('Error deleting last message:', error); + ctx.reply('An error occurred while deleting the last message. Please try again later.'); + } + }); + } + static sendMessage(ctx, key) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + try { + const user = (yield db_1.UserBase.findOne({ + where: { + id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id, + }, + })); + if (user !== null) { + const lang = user.language; + const messages = this.loadMessages(lang); + const message = messages[key]; + if (message) { + // Определяем, есть ли у сообщения фотография + const hasPhoto = message.photo !== ''; + const hasFile = message.file !== ''; + if (hasPhoto && + message.buttons && + message.buttons.length > 0 && + message.photo) { + const photoPath = path_1.default.join(__dirname, message.photo); + if (message.buttons && message.buttons.length > 0) { + const rows = message.buttons; + if (Array.isArray(rows) && + rows.every(row => Array.isArray(row))) { + const sentMessage = yield ctx.replyWithPhoto({ source: fs.createReadStream(photoPath) || '' }, Object.assign({ protect_content: true, caption: message.text, parse_mode: 'HTML' }, telegraf_1.Markup.inlineKeyboard(rows.map(row => row.map(button => { + if (button.url) { + return telegraf_1.Markup.button.url(button.text, button.url); + } + else { + return telegraf_1.Markup.button.callback(button.text, button.callback); + } + }))))); + if (sentMessage.message_id) { + yield db_1.UserBase.update({ message_id: sentMessage.message_id }, { + where: { + id: (_b = ctx.from) === null || _b === void 0 ? void 0 : _b.id, + }, + }); + } + } + else { + console.error('rows должен быть массивом массивов'); + } + } + } + else if (hasPhoto && message.photo) { + const photoPath = path_1.default.join(__dirname, message.photo); + const sentMessage = yield ctx.replyWithPhoto({ source: fs.createReadStream(photoPath) || '' }, { + protect_content: true, + caption: message.text, + parse_mode: 'HTML', + }); + if (sentMessage.message_id) { + yield db_1.UserBase.update({ message_id: sentMessage.message_id }, { + where: { + id: (_c = ctx.from) === null || _c === void 0 ? void 0 : _c.id, + }, + }); + } + } + else if (hasFile && message.file) { + const pdfFilePath = path_1.default.join(__dirname, message.file); + const fileName = path_1.default.basename(pdfFilePath); + // Отправляем файл + const sentMessage = yield ctx.replyWithDocument({ + source: fs.createReadStream(pdfFilePath) || '', + filename: fileName, + }, { + protect_content: true, + caption: message.text, + parse_mode: 'HTML', + }); + if (sentMessage.message_id) { + yield db_1.UserBase.update({ message_id: sentMessage.message_id }, { + where: { + id: (_d = ctx.from) === null || _d === void 0 ? void 0 : _d.id, + }, + }); + } + } + else { + // Отправляем текст и кнопки, если они есть + if (message.buttons && message.buttons.length > 0) { + const rows = message.buttons; + if (Array.isArray(rows) && + rows.every(row => Array.isArray(row))) { + const sentMessage = yield ctx.reply(message.text, Object.assign({ protect_content: true, parse_mode: 'HTML' }, telegraf_1.Markup.inlineKeyboard(rows.map(row => row.map(button => { + if (button.url) { + return telegraf_1.Markup.button.url(button.text, button.url); + } + else { + return telegraf_1.Markup.button.callback(button.text, button.callback); + } + }))))); + if (sentMessage.message_id) { + yield db_1.UserBase.update({ message_id: sentMessage.message_id }, { + where: { + id: (_e = ctx.from) === null || _e === void 0 ? void 0 : _e.id, + }, + }); + } + } + else { + console.error('rows должен быть массивом массивов'); + } + } + else { + // Отправляем только текст, если нет кнопок + const sentMessage = yield ctx.replyWithHTML(message.text, { + protect_content: true, + }); + yield db_1.UserBase.update({ message_id: (_g = (_f = ctx.callbackQuery) === null || _f === void 0 ? void 0 : _f.message) === null || _g === void 0 ? void 0 : _g.message_id }, { + where: { + id: (_h = ctx.from) === null || _h === void 0 ? void 0 : _h.id, + }, + }); + if (sentMessage.message_id) { + yield db_1.UserBase.update({ message_id: sentMessage.message_id }, { + where: { + id: (_j = ctx.from) === null || _j === void 0 ? void 0 : _j.id, + }, + }); + } + } + } + } + else { + console.error(`Key not found: ${key}`); + } + } + else { + console.error('User not found.'); + } + } + catch (error) { + console.error('Error fetching user language:', error); + console.log('An error occurred. Please try again later.'); + } + }); + } +} +exports.MessagesService = MessagesService; +MessagesService.messages = {}; diff --git a/mtucijobsbot/docker-compose.yml b/mtucijobsbot/docker-compose.yml new file mode 100644 index 0000000..4472602 --- /dev/null +++ b/mtucijobsbot/docker-compose.yml @@ -0,0 +1,62 @@ +version: '3.9' + +x-common-logging: &common-logging + logging: + # limit logs retained on host to 25MB + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +x-common-network: &common-network + networks: + - mtuci-jobs + +services: + bot-mtuci-jobs: + image: image-mtuci-jobs + build: + context: . + dockerfile: Dockerfile + container_name: bot-mtuci-jobs + restart: always + depends_on: + postgres-mtuci-jobs: + condition: service_healthy + ports: + - '127.0.0.1:3005:3005' + - '127.0.0.1:3006:3006' + environment: + DATABASE_URL: 'postgres://postgres:password@postgres-mtuci-jobs:5432/mtuci-jobs' + DB_NAME: mtucijobs + DB_USER: postgres + DB_PASSWORD: password + DB_HOST: postgres-mtuci-jobs + BOT_TOKEN: '7306496213:AAGuvha0ytpF8keCZpes8BdP1AnfP7yEiSE' + HOOKPORT: 3005 + PORT: 3006 + DOMAIN: '' + WEB_APP: '' + <<: [*common-network, *common-logging] + + postgres-mtuci-jobs: + image: postgres:16.1-alpine3.19 + container_name: bot-postgres-mtuci-jobs + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: mtucijobs + PGDATA: /data/postgres + POSTGRES_HOST_AUTH_METHOD: trust + healthcheck: + test: pg_isready -U postgres + interval: 5s + timeout: 5s + retries: 5 + volumes: + - /data/mtuci-jobs/data/postgres:/data/postgres + <<: [*common-network, *common-logging] + +networks: + mtuci-jobs: + driver: bridge diff --git a/mtucijobsbot/package-lock.json b/mtucijobsbot/package-lock.json new file mode 100644 index 0000000..0d9559f --- /dev/null +++ b/mtucijobsbot/package-lock.json @@ -0,0 +1,3007 @@ +{ + "name": "mtucijobs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mtucijobs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.2", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "form-data": "^4.0.0", + "fs": "^0.0.1-security", + "path": "^0.12.7", + "pg": "^8.12.0", + "sequelize": "^6.37.3", + "telegraf": "^4.16.3", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + }, + "devDependencies": { + "@types/axios": "^0.14.0", + "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/form-data": "^2.5.0", + "@types/node": "^20.14.2", + "copyfiles": "^2.4.1", + "eslint": "^9.4.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.15.1.tgz", + "integrity": "sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz", + "integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.3.tgz", + "integrity": "sha512-HAbhAYKfsAC2EkTqve00ibWIZlaU74Z1EHwAjYr4PXF0YU2VEA1zSIKSSpKszRLRWwHzzRZXvK632u+uXzvsvw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@telegraf/types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", + "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "dependencies": { + "axios": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==", + "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "dotenv": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", + "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/form-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.5.0.tgz", + "integrity": "sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==", + "deprecated": "This is a stub types definition. form-data provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "form-data": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.11.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", + "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.4.0.tgz", + "integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/config-array": "^0.15.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.4.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/espree": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", + "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-compare": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", + "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", + "dependencies": { + "buffer-alloc": "^1.2.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sandwich-stream": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", + "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/sequelize": { + "version": "6.37.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz", + "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/telegraf": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", + "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", + "dependencies": { + "@telegraf/types": "^7.1.0", + "abort-controller": "^3.0.0", + "debug": "^4.3.4", + "mri": "^1.2.0", + "node-fetch": "^2.7.0", + "p-timeout": "^4.1.0", + "safe-compare": "^1.1.4", + "sandwich-stream": "^2.0.2" + }, + "bin": { + "telegraf": "lib/cli.mjs" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/telegraf/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/telegraf/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/mtucijobsbot/package.json b/mtucijobsbot/package.json new file mode 100644 index 0000000..0ab801d --- /dev/null +++ b/mtucijobsbot/package.json @@ -0,0 +1,37 @@ +{ + "name": "mtucijobs", + "version": "1.0.0", + "description": "This bot for afree", + "main": "index.js", + "scripts": { + "build": "tsc && copyfiles -u 1 src/**/*.json src/assets/**/* dist", + "start": "node ./dist/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "student-programmer", + "license": "ISC", + "dependencies": { + "axios": "^1.7.2", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "form-data": "^4.0.0", + "fs": "^0.0.1-security", + "path": "^0.12.7", + "pg": "^8.12.0", + "sequelize": "^6.37.3", + "telegraf": "^4.16.3", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + }, + "devDependencies": { + "@types/axios": "^0.14.0", + "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/form-data": "^2.5.0", + "@types/node": "^20.14.2", + "copyfiles": "^2.4.1", + "eslint": "^9.4.0" + } +} diff --git a/mtucijobsbot/run.sh b/mtucijobsbot/run.sh new file mode 100644 index 0000000..c8505e6 --- /dev/null +++ b/mtucijobsbot/run.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +docker stop mtuci-jobs || echo "1" +docker rm mtuci-jobs || echo "2" + +docker run -d \ + --restart always \ + -h mtuci-jobs \ + --name mtuci-jobs \ + -e BOT_TOKEN="7306496213:AAGuvha0ytpF8keCZpes8BdP1AnfP7yEiSE" \ + --log-opt mode=non-blocking \ + --log-opt max-size=10m \ + --log-opt max-file=3 \ + mtuci-jobs-image:latest \ No newline at end of file diff --git a/mtucijobsbot/src/api/resume.ts b/mtucijobsbot/src/api/resume.ts new file mode 100644 index 0000000..049b761 --- /dev/null +++ b/mtucijobsbot/src/api/resume.ts @@ -0,0 +1,34 @@ +import express from 'express'; + +import 'dotenv/config'; + + +const router = express.Router(); + +router.post('/api/resume', async (req, res) => { + try { + const postData = req.body; + + // Проверяем наличие chatId в теле запроса + if (!postData.chatId) { + throw new Error('Chat ID is missing in the request body'); + } + + // Ваша логика обработки данных + console.log('Received data:', postData.id); + + // Отправка сообщения в боте + // await req.bot.telegram.sendMessage( + // postData.chatId, + // `Received data: ${postData.id}` + // ); + // MessagesService.sendMessage(postData.id, ) + + res.status(200).json({ message: 'Data received successfully' }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +export default router; diff --git a/mtucijobsbot/src/assets/шаблон МТУСИ.docx b/mtucijobsbot/src/assets/шаблон МТУСИ.docx new file mode 100644 index 0000000000000000000000000000000000000000..cb2b7570c7cd2d7c45c91f2dc754f8ab1034761e GIT binary patch literal 37363 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 B@7DkT literal 0 HcmV?d00001 diff --git a/mtucijobsbot/src/commands/start.ts b/mtucijobsbot/src/commands/start.ts new file mode 100644 index 0000000..b026257 --- /dev/null +++ b/mtucijobsbot/src/commands/start.ts @@ -0,0 +1,90 @@ +import { Context } from 'telegraf'; +import { MenuController } from '../modules/menuController'; +import { Resume, UserBase, sequelize } from '../db/db'; +import { UserRole } from '../models/User'; +import axios from 'axios'; + +export async function startCommand(ctx: Context): Promise { + function isBotOwner(userId: number | undefined): boolean { + const ownerUserId = 1053404914; // Замените этот ID на фактический ID владельца + return userId === ownerUserId; + } + + try { + await sequelize.authenticate(); // Проверка подключения к базе данных + await UserBase.sync(); // Создание таблицы, если её нет + console.log('User table has been created or already exists.'); + + // Проверка, существует ли пользователь в базе данных + const [userInstance, created] = await UserBase.findOrCreate({ + where: { id: ctx.from?.id }, + defaults: { + id: ctx.from?.id || 0, + username: ctx.from?.username || '', + vacansyindex: 0, + role: isBotOwner(ctx.from?.id) ? UserRole.OWNER : UserRole.CLIENT, + language: ctx.from?.language_code || 'ru', + message_id: ctx.callbackQuery?.message?.message_id || 0, + last_obj: '', + chat_id: ctx.chat?.id, + resume_id: null, + }, + }); + + // Выполняем запрос на удаление резюме, если оно существует + try { + const response = await axios.delete( + `${process.env.API}students/${ctx.from?.id}`, + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + } + ); + + if (response.status === 204) { + console.log('Резюме успешно удалено на сервере.'); + await Resume.destroy({ where: { id: ctx.from?.id } }); // Удаляем запись из базы данных + await ctx.reply('Ваше резюме было удалено.'); + } else { + console.warn( + 'Не удалось удалить резюме, сервер вернул статус:', + response.status + ); + await ctx.reply('Не удалось удалить ваше резюме. Попробуйте позже.'); + } + } catch (error) { + console.error('Ошибка при удалении резюме:', error); + await ctx.reply( + 'Произошла ошибка при удалении резюме. Пожалуйста, попробуйте позже.' + ); + } + + // Приветственное сообщение + const greetingMessage = { + text: + 'Вас приветствует бот MTUCI jobs! \n\n' + + 'Для подтверждения того, что вы студент МТУСИ, вам потребуется привязать вашу учетную запись LMS к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты.', + buttons: [[{ text: 'Понял', callback: 'accept2' }]], + }; + + // Отправка приветственного сообщения с кнопками + await ctx.reply(greetingMessage.text, { + reply_markup: { + inline_keyboard: greetingMessage.buttons.map(row => + row.map(button => ({ + text: button.text, + callback_data: button.callback, + })) + ), + }, + parse_mode: 'HTML', + }); + + console.log('Бот запущен'); + } catch (e) { + console.error('Произошла ошибка при запуске бота', e); + ctx.reply('Произошла ошибка при запуске. Пожалуйста, попробуйте позже.'); + } +} diff --git a/mtucijobsbot/src/db/db.ts b/mtucijobsbot/src/db/db.ts new file mode 100644 index 0000000..183d8a1 --- /dev/null +++ b/mtucijobsbot/src/db/db.ts @@ -0,0 +1,176 @@ +import 'dotenv/config'; + +import { DataTypes, Sequelize, Model } from 'sequelize'; + + +const sequelize = new Sequelize( + process.env.DB_NAME || 'custdev', + process.env.DB_USER || 'postgres', + process.env.DB_PASSWORD, + { + host: process.env.DB_HOST, + dialect: 'postgres', + } +); + +// Определение интерфейса для модели +interface UserAttributes { + id: number; + username: string; + vacansyindex: number; + role: string; + language: string; + message_id: number | null; + last_obj: string | null; + chat_id: number | null; + resume_id: number | null; // добавляем внешний ключ +} + +interface UserCreationAttributes extends UserAttributes {} + +class UserBase + extends Model + implements UserAttributes +{ + public id!: number; + public username!: string; + public vacansyindex!: number; + public role!: string; + public language!: string; + public message_id!: number | null; + public last_obj!: string | null; + public chat_id!: number | null; + public resume_id!: number | null; // добавляем внешний ключ +} +UserBase.init( + { + id: { + type: DataTypes.BIGINT, + primaryKey: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + }, + vacansyindex: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + role: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'client', + }, + language: { + type: DataTypes.STRING, + allowNull: false, + }, + message_id: { + type: DataTypes.INTEGER, + allowNull: true, + }, + last_obj: { + type: DataTypes.STRING, + allowNull: true, + }, + chat_id: { + type: DataTypes.BIGINT, + allowNull: true, + }, + resume_id: { + type: DataTypes.BIGINT, + allowNull: true, + references: { + model: 'Resumes', // имя таблицы резюме + key: 'id', + }, + }, + }, + { + sequelize, + modelName: 'User', + } +); +interface ResumeAttributes { + id: number; + name: string; + group: string; + type: string; + skills: string; + softskills: string; + email: string; + resumefile: string; +} + +interface ResumeCreationAttributes extends ResumeAttributes {} + +class Resume + extends Model + implements ResumeAttributes +{ + public id!: number; + public name!: string; + public group!: string; + public type!: string; + public skills!: string; + public softskills!: string; + public email!: string; + public resumefile!: string; +} +Resume.init( + { + id: { + type: DataTypes.BIGINT, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + group: { + type: DataTypes.STRING, + allowNull: false, + }, + + type: { + type: DataTypes.STRING, + allowNull: false, + }, + skills: { + type: DataTypes.STRING, + allowNull: false, + }, + softskills: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + }, + resumefile: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'Resume', + } +); +UserBase.belongsTo(Resume, { foreignKey: 'resume_id', as: 'resume' }); +Resume.hasOne(UserBase, { foreignKey: 'resume_id', as: 'user' }); + + + +export { sequelize, UserBase, Resume }; + +sequelize + .sync() + .then(() => { + console.log('Model has been synchronized successfully.'); + }) + .catch(error => { + console.error('Error synchronizing model:', error); + }); diff --git a/mtucijobsbot/src/index.ts b/mtucijobsbot/src/index.ts new file mode 100644 index 0000000..3da967e --- /dev/null +++ b/mtucijobsbot/src/index.ts @@ -0,0 +1,469 @@ +import 'dotenv/config'; +import { Markup, Scenes, Telegraf, session } from 'telegraf'; +import { startCommand } from './commands/start'; +import { menuSceneMiddleware } from './modules/menuScenes'; +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import { events } from './modules/scenes/events'; +import { Resume } from './db/db'; + +import FormData from 'form-data'; +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import { saveVacansy } from './modules/scenes/savevacansy'; +import { accept } from './modules/scenes/accept'; + +export type MyWizardContext = Scenes.WizardContext; + +const app = express(); +declare global { + namespace Express { + interface Request { + bot: Telegraf; + } + } +} +const bot = new Telegraf(process.env.BOT_TOKEN as string); + +app.use((req: Request, res: Response, next: NextFunction) => { + req.bot = bot; + next(); +}); +app.use(express.json()); +app.use(cors()); +bot.start(startCommand); + +app.post('/api/resume', async (req: Request, res: Response) => { + try { + const postData = req.body; + const resumeTemplatePath = path.resolve( + __dirname, + 'assets', + 'шаблон МТУСИ.docx' + ); + await bot.telegram.sendDocument( + postData.id, + { source: resumeTemplatePath }, + { caption: 'Пример резюме' } + ); + + await bot.telegram.sendMessage( + postData.id, + `Ваша анкета заполнена! Отправьте своё резюме для завершения регистрации`, + { + reply_markup: { + inline_keyboard: [ + [ + { text: 'Загрузить резюме', callback_data: 'uploadresume' }, + // { text: 'Пропустить загрузку', callback_data: 'skip' }, + ], + ], + }, + } + ); + + console.log(`Ваш id ${postData.id}`); + return res.json({ status: 'success' }); + } catch (e) { + console.error('Error:', e); + res.status(500).json({ error: 'An error occurred' }); + throw e; + } +}); +// const resumeScene: Scenes.WizardScene = new Scenes.WizardScene( +// 'resumeScene', +// async ctx => { +// await ctx.reply( +// 'Отправьте своё резюме в PDF формате или отправьте /cancel для отмены.' +// ); +// return ctx.wizard.next(); +// }, + +// async ctx => { +// if ( +// ctx.message && +// 'text' in ctx.message && +// ctx.from && +// ctx.message.text == '/cancel' +// ) { +// await ctx.reply('Отправка резюме отменена.'); +// await ctx.reply( +// 'Меню', +// Markup.keyboard([ +// ['Моя анкета'], +// ['Вакансии'], +// ['Уведомления: включены'], +// ]).resize() +// ); +// return ctx.scene.leave(); +// } + +// if (ctx.message && 'document' in ctx.message && ctx.from) { +// const file = ctx.message.document; + +// if (file.mime_type !== 'application/pdf') { +// ctx.reply('Пожалуйста, отправьте файл в формате PDF.'); +// return; +// } + +// try { +// const fileLink = await ctx.telegram.getFileLink(file.file_id); +// const filePath = path.join(__dirname, `${file.file_id}.pdf`); + +// // Загрузка файла на локальную машину +// const response = await axios.get(fileLink.href, { +// responseType: 'stream', +// }); +// response.data +// .pipe(fs.createWriteStream(filePath)) +// .on('finish', async () => { +// // Создание формы данных +// const form = new FormData(); +// form.append('file', fs.createReadStream(filePath)); + +// try { +// // Установка времени ожидания (в миллисекундах) +// const uploadResponse = await axios.post( +// `${process.env.API}services/resume/`, +// form, +// { +// headers: { +// ...form.getHeaders(), +// 'X-API-KEY': +// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', +// }, +// timeout: 10000, // Увеличьте время ожидания до 10 секунд +// } +// ); + +// const fileName = uploadResponse.data.filename; + +// // Выполнение PUT-запроса для обновления поля Link у студента +// await axios.patch( +// `${process.env.API}students/${ +// ctx.from?.id +// }?Link=${encodeURIComponent(fileName)}`, +// {}, +// { +// headers: { +// 'X-API-KEY': +// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', +// 'Content-Type': 'application/json', +// }, +// } +// ); + +// await ctx.reply('Резюме успешно загружено.'); +// await ctx.reply( +// 'Меню', +// Markup.keyboard([ +// ['Моя анкета'], +// ['Вакансии'], +// ['Уведомления: включены'], +// ]).resize() +// ); +// return ctx.scene.leave(); +// } catch (uploadError) { +// console.error('Ошибка при загрузке резюме на API:', uploadError); +// await ctx.reply('Произошла ошибка при загрузке резюме.'); +// return ctx.scene.leave(); +// } finally { +// // Удаление временного файла после загрузки +// fs.unlinkSync(filePath); +// } +// }); +// } catch (error) { +// console.error('Ошибка при загрузке файла:', error); +// await ctx.reply('Произошла ошибка при загрузке файла.'); +// } +// } else { +// await ctx.reply('Отправьте файл в формате PDF'); +// } +// } +// ); + +const resumeScene: Scenes.WizardScene = new Scenes.WizardScene( + 'resumeScene', + async ctx => { + await ctx.reply( + 'Отправьте своё резюме в формате Word (DOC/DOCX) или отправьте /cancel для отмены.' + ); + return ctx.wizard.next(); + }, + + async ctx => { + if ( + ctx.message && + 'text' in ctx.message && + ctx.from && + ctx.message.text === '/cancel' + ) { + await ctx.reply('Отправка резюме отменена.'); + await ctx.reply( + 'Меню', + Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + ['Уведомления: включены'], + ]).resize() + ); + return ctx.scene.leave(); + } + + if (ctx.message && 'document' in ctx.message && ctx.from) { + const file = ctx.message.document; + const allowedMimeTypes = [ + // 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]; + + if (!allowedMimeTypes.includes(file.mime_type || '')) { + await ctx.reply('Пожалуйста, отправьте файл в формате DOC или DOCX.'); + return; + } + + try { + const fileLink = await ctx.telegram.getFileLink(file.file_id); + const fileExtension = + file.mime_type === 'application/pdf' + ? 'pdf' + : file.mime_type === 'application/msword' + ? 'doc' + : 'docx'; + const filePath = path.join( + __dirname, + `${file.file_id}.${fileExtension}` + ); + + // Загрузка файла на локальную машину + const response = await axios.get(fileLink.href, { + responseType: 'stream', + }); + response.data + .pipe(fs.createWriteStream(filePath)) + .on('finish', async () => { + const form = new FormData(); + form.append('file', fs.createReadStream(filePath)); + + try { + const uploadResponse = await axios.post( + `${process.env.API}services/resume/`, + form, + { + headers: { + ...form.getHeaders(), + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + timeout: 10000, + } + ); + + const fileName = uploadResponse.data.filename; + + await axios.patch( + `${process.env.API}students/${ + ctx.from?.id + }?Link=${encodeURIComponent(fileName)}`, + {}, + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + 'Content-Type': 'application/json', + }, + } + ); + + await ctx.reply('Резюме успешно загружено.'); + await ctx.reply( + 'Меню', + Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + ['Уведомления: включены'], + ]).resize() + ); + return ctx.scene.leave(); + } catch (uploadError) { + console.error('Ошибка при загрузке резюме на API:', uploadError); + await ctx.reply('Произошла ошибка при загрузке резюме.'); + return ctx.scene.leave(); + } finally { + fs.unlinkSync(filePath); + } + }); + } catch (error) { + console.error('Ошибка при загрузке файла:', error); + await ctx.reply('Произошла ошибка при загрузке файла.'); + } + } else { + await ctx.reply( + 'Пожалуйста, отправьте файл в формате PDF, DOC или DOCX.' + ); + } + } +); + +// app.post('/api/resume', async (req: Request, res: Response) => { +// try { +// const postData = req.body; + +// // Проверяем наличие chatId в теле запроса +// if (!postData.StudentID) { +// throw new Error('Chat ID is missing in the request body'); +// } + +// const [userInstance, created] = await Resume.findOrCreate({ +// where: { id: postData.StudentID }, +// defaults: { +// id: postData.StudentID || 0, +// name: postData.Name, +// group: postData.Group, +// type: postData.Type, +// skills: '', +// softskills: postData.Soft_skills, +// email: postData.Email, +// }, +// }); +// await UserBase.update( +// { resume_id: postData.StudentID }, +// { where: { id: postData.StudentID } } +// ); +// if (userInstance instanceof Resume) { +// // userInstance теперь имеет тип User +// if (created) { +// console.log('New user created:', userInstance); +// } else { +// console.log('User already exists:', userInstance); +// } + +// console.log('Привет! Вы успешно зарегистрированы.'); +// } else { +// console.error('Ошибка: userInstance не является экземпляром User.'); +// } + +// console.log('Received data:', postData); + +// res.status(200).json({ message: 'Data received successfully' }); +// } catch (error) { +// console.error(error); +// res.status(500).json({ error: 'Internal Server Error' }); +// } +// }); +app.post('/webhook', async (req: Request, res: Response) => { + try { + const postData = req.body; + const entityType = postData.data.entity_type; + const updatedMatches = postData.data.updated_matches; + console.log(postData); + + if (entityType === 'job') { + for (const match of updatedMatches) { + const chatId = match.student_id; + const jobId = match.entity_id; // Получаем ID вакансии + const messageText = + 'Ваше совпадение с вакансией было обновлено. Вот детали вакансии:'; + + if (!chatId) { + throw new Error('Неправильный Chat ID (student_id) в теле запроса.'); + } + + // Выполняем GET-запрос для получения данных о вакансии + const response = await axios.get( + `${process.env.API}jobs/${jobId}`, // Запрашиваем данные о вакансии по её ID + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + } + ); + + const vacancyData = response.data; // Данные о вакансии + console.log(vacancyData); + + // Отправляем вакансию пользователю + await sendVacancyToUser(chatId, vacancyData); + + console.log( + `Сообщение с вакансией отправлено пользователю с ID ${chatId}` + ); + } + } + + return res.status(200).json({ status: 'success' }); + } catch (e) { + console.error('Error:', e); + res.status(500).json({ error: 'Произошла ошибка при отправке сообщения' }); + } +}); + +// Функция для отправки вакансии пользователю +async function sendVacancyToUser(chatId: number, data: any) { + await bot.telegram.sendMessage( + chatId, + `${data.Job_name}\n\n` + + `Компания: ${data.Company_name}\n` + + `Заработная плата: ${data.Salary} руб/мес\n` + + `Контактные данные: ${data.Email}\n\n` + + `Требования к кандидату:\n` + + ` - ${data.Year} курс\n` + + ` - Опыт работы по специальности: ${data.Qualification}\n` + + ` - Soft skills: ${data.Soft_skills}\n` + + `Обязанности:\n` + + `${data.Responsibilities}`, + { + parse_mode: 'HTML', // Используем HTML для форматирования текста + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Сохранить вакансию', + callback_data: `savevacancy+${data.JobID}`, + }, + ], + ], + }, + } + ); +} + +// Обработчик для получения файла резюме + +const stage = new Scenes.Stage([resumeScene]); +bot.use(session()); +bot.use(stage.middleware()); +bot.use(menuSceneMiddleware); +events(bot); +accept(bot); + +bot.action('uploadresume', async ctx => { + ctx.scene.enter('resumeScene'); + ctx.answerCbQuery(); +}); + +saveVacansy(bot); + +bot + .launch({ + webhook: { + domain: process.env.DOMAIN || '', + port: parseInt(process.env.HOOKPORT || ''), + }, + }) + .then(() => + console.log( + 'Webhook bot listening on port', + parseInt(process.env.HOOKPORT || '') + ) + ); +// bot.launch().then(() => { +// console.log('Бот запущен'); +// }); + +app.listen(process.env.PORT, () => { + console.log(`Server is running at http://localhost:${process.env.PORT}`); +}); diff --git a/mtucijobsbot/src/languages/en.json b/mtucijobsbot/src/languages/en.json new file mode 100644 index 0000000..88a8207 --- /dev/null +++ b/mtucijobsbot/src/languages/en.json @@ -0,0 +1,24 @@ +{ + "greeting": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept" }, + { "text": "Понял, не запрашивать данные", "callback": "accept" } + ] + ] + }, + "greeting2": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept2" }, + { "text": "Понял, не запрашивать данные", "callback": "accept2" } + ] + ] + } +} diff --git a/mtucijobsbot/src/languages/ru.json b/mtucijobsbot/src/languages/ru.json new file mode 100644 index 0000000..ec84b86 --- /dev/null +++ b/mtucijobsbot/src/languages/ru.json @@ -0,0 +1,24 @@ +{ + "greeting": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept" }, + { "text": "Понял, не запрашивать данные", "callback": "accept" } + ] + ] + }, + "greeting2": { + "photo": "", + "file": "", + "text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты", + "buttons": [ + [ + { "text": "Понял", "callback": "accept2" }, + { "text": "Понял, не запрашивать данные", "callback": "accept2" } + ] + ] + } +} diff --git a/mtucijobsbot/src/models/User.ts b/mtucijobsbot/src/models/User.ts new file mode 100644 index 0000000..5151e21 --- /dev/null +++ b/mtucijobsbot/src/models/User.ts @@ -0,0 +1,23 @@ +// Интерфейс, представляющий структуру пользователя +export interface User { + id: number; // Уникальный идентификатор пользователя + username: string; // Имя пользователя + password: string; // Количество очков пользователя + email: string; + isAuth:boolean; + points:number; + //referrerId?: number; // Идентификатор пригласившего пользователя (необязательно) + role: UserRole; // Роль пользователя (владелец, админ, модератор, клиент) + language: string; // Язык, на котором взаимодействует пользователь с ботом + message_id?: number; + last_obj?: string; + chat_id: number; +} + +// Перечисление ролей пользователя +export enum UserRole { + OWNER = 'owner', + ADMIN = 'admin', + MODERATOR = 'moderator', + CLIENT = 'client', +} diff --git a/mtucijobsbot/src/modules/menuController.ts b/mtucijobsbot/src/modules/menuController.ts new file mode 100644 index 0000000..cfb1d9e --- /dev/null +++ b/mtucijobsbot/src/modules/menuController.ts @@ -0,0 +1,43 @@ +import { Context, Markup } from 'telegraf'; +import { UserBase } from '../db/db'; +import { MessagesService } from '../services/messagesService'; + +export class MenuController { + static async showLanguageMenu(ctx: Context): Promise { + // await MessagesService.sendMessage(ctx, 'greeting'); + } + + static async setLanguage(ctx: Context): Promise { + try { + if (!ctx.callbackQuery) { + throw new Error('CallbackQuery is not defined.'); + } + + const languageCode = + ctx.callbackQuery && 'data' in ctx.callbackQuery + ? ctx.callbackQuery.data.split('_')[1] + : undefined; + + if (languageCode) { + // Обновляем язык пользователя в базе данных + await UserBase.update( + { language: languageCode }, + { where: { id: ctx.from?.id } } + ); + + // Обновляем язык в объекте контекста + if (ctx.from) { + ctx.from.language_code = languageCode; + } + } else { + // Если data отсутствует в callbackQuery, обработка не может быть выполнена + throw new Error('Missing data property in callbackQuery.'); + } + } catch (error) { + console.error('Error during setLanguage:', error); + console.log( + 'Произошла ошибка при обновлении языка. Пожалуйста, попробуйте снова позже.' + ); + } + } +} diff --git a/mtucijobsbot/src/modules/menuScenes.ts b/mtucijobsbot/src/modules/menuScenes.ts new file mode 100644 index 0000000..1c0ac85 --- /dev/null +++ b/mtucijobsbot/src/modules/menuScenes.ts @@ -0,0 +1,39 @@ +import { Context, MiddlewareFn, Scenes } from 'telegraf'; +import { UserBase } from '../db/db'; +import { MessagesService } from '../services/messagesService'; + +// Middleware для обработки callback'ов +const callbackQueryMiddleware: MiddlewareFn = async ( + ctx, + next +) => { + try { + + const languageCode = + ctx.callbackQuery && 'data' in ctx.callbackQuery + ? ctx.callbackQuery.data.split('_')[1] + : undefined; + // Обновляем язык пользователя в базе данных + if (languageCode && ctx.from?.id) { + await UserBase.update( + { language: languageCode }, + { where: { id: ctx.from.id } } + ); + + // Отправляем сообщение об успешном обновлении языка + console.log(`Язык обновлен: ${languageCode}`); + await ctx.answerCbQuery(); + await MessagesService.sendMessage(ctx, 'greeting'); + } + } catch (error) { + console.error('Error during callback handling:', error); + console.log( + "Произошла ошибка при обработке callback'а. Пожалуйста, попробуйте снова позже." + ); + } + + // Вызываем следующий middleware + return next(); +}; + +export const menuSceneMiddleware = callbackQueryMiddleware; diff --git a/mtucijobsbot/src/modules/scenes/accept.ts b/mtucijobsbot/src/modules/scenes/accept.ts new file mode 100644 index 0000000..f9a25df --- /dev/null +++ b/mtucijobsbot/src/modules/scenes/accept.ts @@ -0,0 +1,107 @@ +import axios from 'axios'; +import { Scenes, Telegraf } from 'telegraf'; +import 'dotenv/config'; + +// const API_BASE_URL = 'https://mtucitech.ru/api/external/mtuci_jobs'; +// const BEARER_TOKEN = 'zKbgaXQv{pvGtQm~U9$urtD#QsiHc@Ie'; + +export function accept(bot: Telegraf) { + bot.action('accept', async ctx => { + try { + const response = await axios.get( + `${process.env.MTUCI_TECH}/check?telegram_id=${ctx.from.id}`, + { + headers: { + Authorization: `Bearer ${process.env.BEARER_TOKEN}`, + }, + } + ); + const data = response.data; + + // Логика обработки ответа + switch (data.status) { + case 338: + ctx.answerCbQuery( + 'Ошибка на стороне сервера. Пожалуйста, сообщите нам.' + ); + break; + case 339: + ctx.answerCbQuery('Пользователь не привязан к проекту.'); + + break; + case 340: + ctx.reply('Можете заполнить своё резюме', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Резюме', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + ctx.answerCbQuery('Пользователь найден, но не привязан LMS.'); + + break; + case 341: + ctx.reply('Можете заполнить своё резюме', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Резюме', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + ctx.answerCbQuery( + 'Пользователь найден, LMS привязан. Можно запросить дополнительные данные.' + ); + default: + ctx.answerCbQuery('Неизвестный статус.'); + break; + } + } catch (error) { + if (axios.isAxiosError(error)) { + // Обработка ошибок Axios + if (error.response && error.response.status === 409) { + ctx.answerCbQuery('Вакансия уже добавлена в избранное'); + } else { + ctx.answerCbQuery('Произошла ошибка'); + console.error(error.response?.data || error.message); + } + } else { + // Обработка других типов ошибок + ctx.answerCbQuery('Произошла неизвестная ошибка'); + console.error(error); + } + } + }); + + bot.action('accept2', async ctx => { + + ctx.reply('Можете заполнить своё резюме', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Резюме', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + ctx.answerCbQuery() + }); +} diff --git a/mtucijobsbot/src/modules/scenes/events.ts b/mtucijobsbot/src/modules/scenes/events.ts new file mode 100644 index 0000000..e81bb95 --- /dev/null +++ b/mtucijobsbot/src/modules/scenes/events.ts @@ -0,0 +1,366 @@ +import { Telegraf, Context, Scenes, Markup } from 'telegraf'; +import { MessagesService } from '../../services/messagesService'; +import { Resume, UserBase } from '../../db/db'; +import axios from 'axios'; +import fs from 'fs'; +import path from 'path'; +import { MyWizardContext } from '../..'; + +interface Vacancies { + JobID: number; + Company_name: string; + Job_name: string; + Year: string; + Qualification: boolean; + Soft_skills: string; + Salary: number; + Email: string; + Archive: boolean; + Responsibilities: string; +} + +let vacancies: Vacancies[] = []; + +export function events(bot: Telegraf) { + bot.action('skip', async ctx => { + ctx.reply( + 'Меню', + Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + // ['Уведомления: включены'], + ]).resize() + ); + ctx.answerCbQuery(); + }); + bot.hears('Вернуться в меню', async ctx => { + ctx.reply( + 'Меню', + Markup.keyboard([ + ['Моя анкета'], + ['Вакансии'], + // ['Уведомления: включены'], + ]).resize() + ); + }); + + bot.hears('Моя анкета', async ctx => { + try { + // Выполнение первого GET-запроса для получения данных пользователя + const response = await axios.get( + `${process.env.API}students/${ctx.from.id}`, + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + } + ); + const data = response.data; + + const resumeFile = await Resume.findOne({ where: { id: ctx.from.id } }); + // Проверяем, существует ли резюме + if (resumeFile && resumeFile.resumefile.length > 1) { + // Выполнение второго GET-запроса для получения файла с резюме + const resumeResponse = await axios({ + method: 'GET', + url: `${process.env.API}services/resume/${resumeFile.resumefile}`, + + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + + responseType: 'stream', + timeout: 10000, + }); + + // Сохраняем файл временно на диск + const resumePath = path.join(__dirname, 'resume.pdf'); + const writer = fs.createWriteStream(resumePath); + resumeResponse.data.pipe(writer); + + writer.on('finish', async () => { + // Отправка файла и сообщения с данными из API + await ctx.replyWithDocument({ source: resumePath }); + + await ctx.replyWithHTML( + `Имя: ${data.Name}\nГруппа: ${ + data.Group + }\nТип: ${ + data.Type + }\nГотов на занятость: ${data.Time.join()}\nО себе: ${ + data.Soft_skills + }\nПочта: ${data.Email}\n`, + { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Редактировать анкету', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + [ + { + text: 'Обновить резюме', + callback_data: 'update_resume', + }, + ], + ], + }, + } + ); + + // Удаляем временный файл + fs.unlinkSync(resumePath); + }); + + writer.on('error', err => { + console.error('Ошибка при записи файла резюме:', err); + ctx.reply('Произошла ошибка при получении файла резюме.'); + }); + } else { + // Отправка только сообщения с данными из API, если резюме не существует + await ctx.replyWithHTML( + `Имя: ${data.Name}\nГруппа: ${ + data.Group + }\nТип: ${ + data.Type + }\nГотов на занятость: ${data.Time.join()}\nО себе: ${ + data.Soft_skills + }\nПочта: ${data.Email}\n`, + { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Редактировать', + web_app: { url: process.env.WEB_APP || '' }, + }, + ], + [ + { + text: 'Загрузить резюме', + callback_data: 'update_resume', + }, + ], + ], + }, + } + ); + } + } catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + }); + + bot.hears('Вакансии', async ctx => { + try { + await ctx.reply( + 'Выберите тип вакансии:', + Markup.keyboard([ + ['Актуальные вакансии'], + ['Сохраненные вакансии'], + ['Поиск'], + ['Вернуться в меню'], + ]).resize() + ); + } catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + }); + bot.hears('Поиск', async ctx => { + try { + await ctx.reply( + 'Поиск вакансий:', + Markup.keyboard([ + ['Актуальные вакансии'], + ['Сохраненные вакансии'], + ['Вернуться в меню'], + ]).resize() + ); + ctx.reply('Страница с поиском вакансий', { + reply_markup: { + inline_keyboard: [ + [ + { + text: 'Перейти', + web_app: { url: process.env.WEB_APP_SEARCH || '' }, + }, + ], + ], + resize_keyboard: true, + one_time_keyboard: true, + }, + }); + } catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + }); + + bot.hears('Актуальные вакансии', async ctx => { + try { + await UserBase.update( + { vacansyindex: 0 }, + { + where: { + id: ctx.from.id, + }, + } + ); + // Выполнение первого GET-запроса для получения данных пользователя + const response = await axios.get( + `${process.env.API}students/matches/${ctx.from.id}`, + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + } + ); + vacancies = response.data; + console.log(vacancies); + + if (vacancies.length === 0) { + ctx.reply('На данный момент нет актуальных вакансий.'); + return; + } + let user = await UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = user?.vacansyindex; + await sendVacancy(ctx, currentIndex, true); + } + } catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('Произошла ошибка при получении данных из API.'); + } + }); + + bot.hears('Сохраненные вакансии', async ctx => { + try { + await UserBase.update( + { vacansyindex: 0 }, + { + where: { + id: ctx.from.id, + }, + } + ); + // Выполнение первого GET-запроса для получения данных пользователя + const response = await axios.get( + `${process.env.API}students/favourites/${ctx.from.id}`, + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + } + ); + vacancies = response.data; + console.log(vacancies); + + if (vacancies.length === 0) { + ctx.reply('На данный момент нет сохранённых вакансий.'); + return; + } + let user = await UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = user?.vacansyindex; + await sendVacancy(ctx, currentIndex, false); + } + } catch (error) { + console.error('Ошибка при выполнении запроса к API:', error); + ctx.reply('На данный момент нет сохранённых вакансий.'); + // ctx.reply('Произошла ошибка при получении данных из API.'); + } + }); + + bot.action('next', async ctx => { + let user = await UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = user?.vacansyindex + 1; + if (currentIndex <= vacancies.length - 1) { + await UserBase.update( + { vacansyindex: currentIndex }, + { where: { id: ctx.from.id } } + ); + await sendVacancy(ctx, currentIndex, true); + ctx.answerCbQuery(); + } else { + ctx.answerCbQuery('Это последняя вакансия.'); + } + } + }); + bot.action('update_resume', async ctx => { + await ctx.scene.enter('resumeScene'); + await ctx.answerCbQuery(); + }); + + bot.action('prev', async ctx => { + let user = await UserBase.findOne({ where: { id: ctx.from.id } }); + if (user) { + let currentIndex = user?.vacansyindex - 1; + if (currentIndex >= 0) { + await UserBase.update( + { vacansyindex: currentIndex }, + { where: { id: ctx.from.id } } + ); + await sendVacancy(ctx, currentIndex, true); + ctx.answerCbQuery(); + } else { + ctx.answerCbQuery('Это первая вакансия.'); + } + } + }); + +async function sendVacancy( + ctx: MyWizardContext, + index: number, + showSaveButton: boolean +) { + const data = vacancies[index]; + + // Формируем кнопки навигации + let inlineKeyboard = [ + [ + { text: 'Назад', callback_data: 'prev' }, + { text: `${index + 1}/${vacancies.length}`, callback_data: 'new' }, + { text: 'Далее', callback_data: 'next' }, + ], + ]; + + // Добавляем кнопку "Сохранить вакансию" только если showSaveButton = true + if (showSaveButton) { + inlineKeyboard.push([ + { + text: 'Сохранить вакансию', + callback_data: `savevacancy+${data.JobID}`, + }, + ]); + } + + // Отправляем сообщение с вакансиями + await ctx.replyWithHTML( + `${data.Job_name}\n\n` + + `Компания: ${data.Company_name}\n` + + `Заработная плата: ${data.Salary} руб/мес\n` + + `Контактные данные: ${data.Email}\n\n` + + `Требования к кандидату:\n` + + ` - ${data.Year} курс\n` + + ` - Опыт работы по специальности: ${data.Qualification}\n` + + ` - Soft skills: ${data.Soft_skills}\n` + + `Обязанности:\n` + + `${data.Responsibilities}`, + { + reply_markup: { + inline_keyboard: inlineKeyboard, + }, + } + ); +} + +} diff --git a/mtucijobsbot/src/modules/scenes/savevacansy.ts b/mtucijobsbot/src/modules/scenes/savevacansy.ts new file mode 100644 index 0000000..d89f93f --- /dev/null +++ b/mtucijobsbot/src/modules/scenes/savevacansy.ts @@ -0,0 +1,49 @@ +import { Telegraf, Context, Scenes, Markup } from 'telegraf'; +import { MessagesService } from '../../services/messagesService'; +import { Resume, UserBase } from '../../db/db'; +import axios from 'axios'; +import 'dotenv/config'; +import fs from 'fs'; +import path from 'path'; +import { MyWizardContext } from '../..'; + +export function saveVacansy(bot: Telegraf) { + bot.on('callback_query', async ctx => { + if (ctx.callbackQuery && 'data' in ctx.callbackQuery) { + const callbackData = ctx.callbackQuery.data; + + if (callbackData.startsWith('savevacancy+')) { + const jobID = callbackData.split('+')[1]; + + try { + const data = await axios.post( + `${process.env.API}students/favourites/`, + { StudentID: ctx.from.id, JobID: jobID }, + { + headers: { + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, + } + ); + console.log(data); + ctx.answerCbQuery('Вакансия была сохранена'); + } catch (error) { + if (axios.isAxiosError(error)) { + // Обработка ошибок Axios + if (error.response && error.response.status === 409) { + ctx.answerCbQuery('Вакансия уже добавлена в избранное'); + } else { + ctx.answerCbQuery('Произошла ошибка при сохранении вакансии'); + console.error(error.response?.data || error.message); + } + } else { + // Обработка других типов ошибок + ctx.answerCbQuery('Произошла неизвестная ошибка'); + console.error(error); + } + } + } + } + }); +} diff --git a/mtucijobsbot/src/services/messagesService.ts b/mtucijobsbot/src/services/messagesService.ts new file mode 100644 index 0000000..a058c64 --- /dev/null +++ b/mtucijobsbot/src/services/messagesService.ts @@ -0,0 +1,371 @@ + // Импортируем необходимые модули + import * as fs from 'fs'; + // import { CustomContext } from '../types/telegram'; + import path from 'path'; + import { UserBase } from '../db/db'; + import { Context, Markup } from 'telegraf'; + + // Определяем тип для объекта сообщений + + type Button = { + text: string; + callback: string; + url?: string; + }; + + // Определяем тип для объекта контента сообщения + type Message = { + text: string; + photo?: string; // Путь к фотографии + file?: string; + buttons?: Button[][]; + }; + + + + // Определяем тип для объекта сообщений + type Messages = { [key: string]: Message }; + + export class MessagesService { + private static messages: { [lang: string]: Messages } = {}; + + public static loadMessages(lang: string): Messages { + if (!this.messages[lang]) { + const filePath = path.join(__dirname, `../languages/${lang}.json`); + const data = fs.readFileSync(filePath, 'utf-8'); + this.messages[lang] = JSON.parse(data); + } + + return this.messages[lang]; + } + + public static async backMessage(ctx: Context) { + try { + const user = (await UserBase.findOne({ + where: { + id: ctx.from?.id, + }, + })) as { + id: number; + username: string; + password: string; + email: string; + isAuth: boolean; + points: number; + role: string; + language: string; + message_id: number; + last_obj: string; + chat_id: number; + } | null; + if (user !== null && ctx.chat) { + await ctx.telegram.copyMessage( + ctx.chat?.id, + ctx.chat?.id, + user.message_id - 1 + ); + } else { + console.log('error'); + } + } catch (error) { + console.log(error); + } + } + public static async hiddenMessage(ctx: Context, key: string) { + try { + const user = (await UserBase.findOne({ + where: { + id: ctx.from?.id, + }, + })) as { + id: number; + username: string; + password: string; + email: string; + isAuth: boolean; + points: number; + role: string; + language: string; + message_id: number; + last_obj: string; + chat_id: number; + } | null; + + if (user && user.message_id && ctx.callbackQuery?.message) { + await ctx.telegram.deleteMessage( + ctx.chat?.id || 0, + ctx.callbackQuery?.message?.message_id + ); + console.log('Hidden message deleted successfully.'); + await UserBase.update( + { last_obj: key }, // Новое значение + { + where: { + id: ctx.from?.id, // Условие для выбора записей + }, + } + ); + } else { + console.log('User or message not found.'); + } + } catch (error) { + console.error('Error deleting last message:', error); + ctx.reply( + 'An error occurred while deleting the last message. Please try again later.' + ); + } + } + + public static async deleteLastMessage(ctx: Context) { + try { + const user = (await UserBase.findOne({ + where: { + id: ctx.from?.id, + }, + })) as { + id: number; + username: string; + password: string; + email: string; + isAuth: boolean; + points: number; + role: string; + language: string; + message_id: number; + last_obj: string; + chat_id: number; + } | null; + + if (user && user.message_id && ctx.callbackQuery?.message) { + await ctx.telegram.deleteMessage( + ctx.chat?.id || 0, + ctx.callbackQuery?.message?.message_id + ); + console.log('Last message deleted successfully.'); + await UserBase.update( + { message_id: ctx.callbackQuery?.message?.message_id }, // Новое значение + { + where: { + id: ctx.from?.id, // Условие для выбора записей + }, + } + ); + } else { + console.log('User or message not found.'); + } + } catch (error) { + console.error('Error deleting last message:', error); + ctx.reply( + 'An error occurred while deleting the last message. Please try again later.' + ); + } + } + + public static async sendMessage( + ctx: Context, + key: string + ) { + try { + const user = (await UserBase.findOne({ + where: { + id: ctx.from?.id, + }, + })) as { + id: number; + username: string; + password: string; + email: string; + isAuth: boolean; + points: number; + role: string; + language: string; + message_id: number; + last_obj: string; + chat_id: number; + } | null; + + if (user !== null) { + const lang = user.language; + const messages = this.loadMessages(lang); + const message = messages[key]; + + if (message) { + // Определяем, есть ли у сообщения фотография + const hasPhoto = message.photo !== ''; + const hasFile = message.file !== ''; + + if ( + hasPhoto && + message.buttons && + message.buttons.length > 0 && + message.photo + ) { + const photoPath = path.join(__dirname, message.photo); + + if (message.buttons && message.buttons.length > 0) { + const rows: Button[][] = message.buttons; + if ( + Array.isArray(rows) && + rows.every(row => Array.isArray(row)) + ) { + const sentMessage = await ctx.replyWithPhoto( + { source: fs.createReadStream(photoPath) || '' }, + { + protect_content: true, + caption: message.text, + parse_mode: 'HTML', + ...Markup.inlineKeyboard( + rows.map(row => + row.map(button => { + if (button.url) { + return Markup.button.url(button.text, button.url); + } else { + return Markup.button.callback( + button.text, + button.callback + ); + } + }) + ) + ), + } + ); + if (sentMessage.message_id) { + await UserBase.update( + { message_id: sentMessage.message_id }, + { + where: { + id: ctx.from?.id, + }, + } + ); + } + } else { + console.error('rows должен быть массивом массивов'); + } + } + } else if (hasPhoto && message.photo) { + const photoPath = path.join(__dirname, message.photo); + const sentMessage = await ctx.replyWithPhoto( + { source: fs.createReadStream(photoPath) || '' }, + { + protect_content: true, + caption: message.text, + parse_mode: 'HTML', + } + ); + if (sentMessage.message_id) { + await UserBase.update( + { message_id: sentMessage.message_id }, + { + where: { + id: ctx.from?.id, + }, + } + ); + } + } else if (hasFile && message.file) { + const pdfFilePath = path.join(__dirname, message.file); + + const fileName = path.basename(pdfFilePath); + + // Отправляем файл + const sentMessage = await ctx.replyWithDocument( + { + source: fs.createReadStream(pdfFilePath) || '', + filename: fileName, + }, + { + protect_content: true, + caption: message.text, + parse_mode: 'HTML', + } + ); + if (sentMessage.message_id) { + await UserBase.update( + { message_id: sentMessage.message_id }, + { + where: { + id: ctx.from?.id, + }, + } + ); + } + } else { + // Отправляем текст и кнопки, если они есть + if (message.buttons && message.buttons.length > 0) { + const rows: Button[][] = message.buttons; + if ( + Array.isArray(rows) && + rows.every(row => Array.isArray(row)) + ) { + const sentMessage = await ctx.reply(message.text, { + protect_content: true, + parse_mode: 'HTML', + ...Markup.inlineKeyboard( + rows.map(row => + row.map(button => { + if (button.url) { + return Markup.button.url(button.text, button.url); + } else { + return Markup.button.callback( + button.text, + button.callback + ); + } + }) + ) + ), + }); + + if (sentMessage.message_id) { + await UserBase.update( + { message_id: sentMessage.message_id }, + { + where: { + id: ctx.from?.id, + }, + } + ); + } + } else { + console.error('rows должен быть массивом массивов'); + } + } else { + // Отправляем только текст, если нет кнопок + const sentMessage = await ctx.replyWithHTML(message.text, { + protect_content: true, + }); + await UserBase.update( + { message_id: ctx.callbackQuery?.message?.message_id }, + { + where: { + id: ctx.from?.id, + }, + } + ); + + if (sentMessage.message_id) { + await UserBase.update( + { message_id: sentMessage.message_id }, + { + where: { + id: ctx.from?.id, + }, + } + ); + } + } + } + } else { + console.error(`Key not found: ${key}`); + } + } else { + console.error('User not found.'); + } + } catch (error) { + console.error('Error fetching user language:', error); + console.log('An error occurred. Please try again later.'); + } + } + } diff --git a/mtucijobsbot/tsconfig.json b/mtucijobsbot/tsconfig.json new file mode 100644 index 0000000..6bc9e20 --- /dev/null +++ b/mtucijobsbot/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/mtucijobsweb/.dockerignore b/mtucijobsweb/.dockerignore new file mode 100644 index 0000000..7425fa4 --- /dev/null +++ b/mtucijobsweb/.dockerignore @@ -0,0 +1,12 @@ +**/node_modules +*.md +Dockerfile +docker-compose.yml +**/.npmignore +**/.dockerignore +**/*.md +**/*.log +**/.vscode +**/.git +**/.eslintrc.json +*.sh \ No newline at end of file diff --git a/mtucijobsweb/.eslintrc.js b/mtucijobsweb/.eslintrc.js new file mode 100644 index 0000000..bb6a5ad --- /dev/null +++ b/mtucijobsweb/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['next', 'next/core-web-vitals'], +}; diff --git a/mtucijobsweb/.gitignore b/mtucijobsweb/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/mtucijobsweb/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/mtucijobsweb/Dockerfile b/mtucijobsweb/Dockerfile new file mode 100644 index 0000000..58e96d8 --- /dev/null +++ b/mtucijobsweb/Dockerfile @@ -0,0 +1,18 @@ +FROM node:21.5.0-alpine3.19 + +ARG NEXT_PUBLIC_APP_BASE_URL +ARG NEXT_PUBLIC_BOT_URL + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NEXT_PUBLIC_APP_BASE_URL=$NEXT_PUBLIC_APP_BASE_URL +ENV NEXT_PUBLIC_BOT_URL=$NEXT_PUBLIC_BOT_URL + +WORKDIR /app +COPY package*.json ./ +RUN npm i +COPY . . + +RUN npm run build + +EXPOSE 3000 +CMD ["npm", "start"] \ No newline at end of file diff --git a/mtucijobsweb/README.md b/mtucijobsweb/README.md new file mode 100644 index 0000000..c403366 --- /dev/null +++ b/mtucijobsweb/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/mtucijobsweb/api/api.ts b/mtucijobsweb/api/api.ts new file mode 100644 index 0000000..6eaf5e3 --- /dev/null +++ b/mtucijobsweb/api/api.ts @@ -0,0 +1,77 @@ +import { FormValues, JobsSearch, Request, Student } from '@/types/types'; +import { $mtuciApi } from './axiosInstance'; + +export const sendStudent = async (postData: Request) => { + try { + const response = await $mtuciApi.post(`/students/`, postData); + return response; + } catch (error) { + console.error('Error post student:', error); + throw error; + } +}; + +export const editStudent = async ( + postData: Omit, + id: number +) => { + try { + const response = await $mtuciApi.put(`/students/${id}`, postData); + return response; + } catch (error) { + console.error('Error post student:', error); + throw error; + } +}; + +export const fetchStudent = async (id: number) => { + try { + const response = await $mtuciApi.get(`/students/${id}`); + return response; + } catch (error) { + console.error('Error fetching student:', error); + throw error; + } +}; +export const deleteStudent = async (id: number) => { + try { + const response = await $mtuciApi.delete(`/students/${id}`); + return response; + } catch (error) { + console.error('Error fetching student:', error); + throw error; + } +}; + +export const fetchHardSkills = async (id: number) => { + try { + const response = await $mtuciApi.get(`/students/hardskills/${id}`); + return response; + } catch (error) { + console.error('Error fetching student:', error); + throw error; + } +}; + +export const searchJobs = async (params: JobsSearch) => { + try { + const response = await $mtuciApi.get('/students/jobs-search/', { params }); + return response.data; + } catch (error) { + console.error('Error search jobs', error); + throw error; + } +}; + + +export const fetchHardSkillsAll = async (): Promise< + { Hard_skillID: number; Title: string }[] +> => { + try { + const response = await $mtuciApi.get(`/services/hardskills/`); + return response.data; + } catch (error) { + console.error('Error fetching hard skills:', error); + throw error; + } +}; \ No newline at end of file diff --git a/mtucijobsweb/api/axiosInstance.ts b/mtucijobsweb/api/axiosInstance.ts new file mode 100644 index 0000000..87633de --- /dev/null +++ b/mtucijobsweb/api/axiosInstance.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + +export const $mtuciApi = axios.create({ + baseURL: process.env.NEXT_PUBLIC_APP_BASE_URL, // NEXT_PUBLIC_ для переменных окружения, доступных на клиенте + headers: { + Accept: '*/*', + 'X-API-KEY': + 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n', + }, +}); diff --git a/mtucijobsweb/api/bot.ts b/mtucijobsweb/api/bot.ts new file mode 100644 index 0000000..9dc99b1 --- /dev/null +++ b/mtucijobsweb/api/bot.ts @@ -0,0 +1,15 @@ +import { Bot } from '@/types/types'; +import axios from 'axios'; + +export const sendDataBot = async (dataBot: Bot) => { + try { + const response = await axios.post( + `${process.env.NEXT_PUBLIC_BOT_URL}/api/resume/`, + dataBot + ); + return response; + } catch (error) { + console.error('Error post student:', error); + throw error; + } +}; diff --git a/mtucijobsweb/app/layout.tsx b/mtucijobsweb/app/layout.tsx new file mode 100644 index 0000000..5fc4fb9 --- /dev/null +++ b/mtucijobsweb/app/layout.tsx @@ -0,0 +1,27 @@ +import ClientProvider from '@/fsd/app/providers/ClientProvider'; +import { TmaSDKLoader } from '@/fsd/app/providers/TmaSDKLoader'; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/mtucijobsweb/app/page.tsx b/mtucijobsweb/app/page.tsx new file mode 100644 index 0000000..c75c3bb --- /dev/null +++ b/mtucijobsweb/app/page.tsx @@ -0,0 +1,20 @@ +'use client'; + +import LoadingPage from '@/fsd/pages/Loading'; +import MainPage from '@/fsd/pages/MainPage'; +import { useInitData } from '@tma.js/sdk-react'; + +export default function Home() { + const initData = useInitData(true); + + + return ( + <> + {initData?.user?.id !== undefined ? ( + + ) : ( + + )} + + ); +} diff --git a/mtucijobsweb/app/search/page.tsx b/mtucijobsweb/app/search/page.tsx new file mode 100644 index 0000000..d3585b6 --- /dev/null +++ b/mtucijobsweb/app/search/page.tsx @@ -0,0 +1,15 @@ +'use client'; + +import LoadingPage from '@/fsd/pages/Loading'; +import SearchPage from '@/fsd/pages/SearchPage'; +import { useInitData } from '@tma.js/sdk-react'; + +const Search = () => { + const initData = useInitData(true); + + return ( + <>{initData?.user?.id !== undefined ? : } + ); +}; + +export default Search; diff --git a/mtucijobsweb/build.sh b/mtucijobsweb/build.sh new file mode 100644 index 0000000..2863fde --- /dev/null +++ b/mtucijobsweb/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +docker build . \ + --build-arg APP_BASE_URL="" \ + --no-cache \ + --rm \ + --pull \ + -t mtuci-jobs-web-image:latest diff --git a/mtucijobsweb/fsd/app/providers/ClientProvider.tsx b/mtucijobsweb/fsd/app/providers/ClientProvider.tsx new file mode 100644 index 0000000..a1164ed --- /dev/null +++ b/mtucijobsweb/fsd/app/providers/ClientProvider.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { PropsWithChildren } from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +const ClientProvider = ({ children }: PropsWithChildren) => { + const queryClient = new QueryClient(); + + return ( + {children} + ); +}; + +export default ClientProvider; diff --git a/mtucijobsweb/fsd/app/providers/TmaSDKLoader.tsx b/mtucijobsweb/fsd/app/providers/TmaSDKLoader.tsx new file mode 100644 index 0000000..1d0a98c --- /dev/null +++ b/mtucijobsweb/fsd/app/providers/TmaSDKLoader.tsx @@ -0,0 +1,13 @@ +'use client'; + +import type { PropsWithChildren } from 'react'; +import { SDKProvider } from '@tma.js/sdk-react'; + + +export function TmaSDKLoader({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/mtucijobsweb/fsd/entities/Resume/data.ts b/mtucijobsweb/fsd/entities/Resume/data.ts new file mode 100644 index 0000000..810eaa2 --- /dev/null +++ b/mtucijobsweb/fsd/entities/Resume/data.ts @@ -0,0 +1,109 @@ +export const skillsOptions = [ + 'C', + 'C++', + 'C#', + 'Java', + 'Python', + 'Ruby', + 'Ruby on Rails', + 'R', + 'Matlab', + 'Django', + 'NetBeans', + 'Scala', + 'JavaScript', + 'TypeScript', + 'Go', + 'Software Development', + 'Application Development', + 'Web Applications', + 'Object Oriented Programming', + 'Aspect Oriented Programming', + 'Concurrent Programming', + 'Mobile Development (iOS)', + 'Mobile Development (Android)', + 'Data Science', + 'Data Analytics', + 'Data Mining', + 'Data Visualization', + 'Machine Learning', + 'TensorFlow', + 'PyTorch', + 'Keras', + 'Theano', + 'Statistical Analysis', + 'Bayesian Analysis', + 'Regression Analysis', + 'Time Series Analysis', + 'Clustering', + 'K-means', + 'KNN', + 'Decision Trees', + 'Random Forest', + 'Dimensionality Reduction', + 'PCA', + 'SVD', + 'Gradient Descent', + 'Stochastic Gradient Descent', + 'Outlier Detection', + 'Frequent Itemset Mining', + 'SQL', + 'NoSQL', + 'SQL Server', + 'MS SQL Server', + 'Apache Hadoop', + 'Apache Spark', + 'Apache Airflow', + 'Apache Impala', + 'Apache Drill', + 'HTML', + 'CSS', + 'React', + 'Angular', + 'Vue.js', + 'Node.js', + 'Express.js', + 'REST', + 'SOAP', + 'Web Platforms', + 'System Architecture', + 'Distributed Computing', + 'AWS', + 'AWS Glue', + 'Azure', + 'Google Cloud Platform', + 'Docker', + 'Kubernetes', + 'UNIX', + 'Linux', + 'Windows', + 'MacOS', + 'Embedded Hardware', + 'Debugging', + 'Unit Testing', + 'Integration Testing', + 'System Testing', + 'Code Review', + 'Git', + 'SVN', + 'CI/CD', + 'Software Documentation', + 'IDE', + 'CASE Tools', + 'Computational Complexity', + 'Algorithm Design', + 'Data Structures', + 'Mathematical Modeling', + 'Statistics', + 'Technical Writing', + 'Technical Support', + 'System Design', + 'System Development', + 'Technical Guidance', + 'Client Interface', + 'Vendor Interface', + 'Emerging Technologies', + 'Jira', + 'Trello', + 'Software Architecture', +]; \ No newline at end of file diff --git a/mtucijobsweb/fsd/pages/Loading.tsx b/mtucijobsweb/fsd/pages/Loading.tsx new file mode 100644 index 0000000..fa44ca4 --- /dev/null +++ b/mtucijobsweb/fsd/pages/Loading.tsx @@ -0,0 +1,15 @@ +'use client' +import React from 'react'; +import { LoadingOutlined } from '@ant-design/icons'; +import { Space, Spin } from 'antd'; + +const LoadingPage: React.FC = () => { + return ( + // + // } /> + // +

Загрузака...

+ ); +}; + +export default LoadingPage; diff --git a/mtucijobsweb/fsd/pages/MainPage.tsx b/mtucijobsweb/fsd/pages/MainPage.tsx new file mode 100644 index 0000000..4ac16b9 --- /dev/null +++ b/mtucijobsweb/fsd/pages/MainPage.tsx @@ -0,0 +1,17 @@ +'use client'; + +import Resume from "../widgets/Resume/Resume"; + + +interface MainPageProps { + id: number; +} +const MainPage: React.FC = ({ id }) => { + return ( +
+ +
+ ); +}; + +export default MainPage; diff --git a/mtucijobsweb/fsd/pages/SearchPage.tsx b/mtucijobsweb/fsd/pages/SearchPage.tsx new file mode 100644 index 0000000..be56f25 --- /dev/null +++ b/mtucijobsweb/fsd/pages/SearchPage.tsx @@ -0,0 +1,13 @@ +'use client' + +import SearchWidget from '../widgets/Search/SearchWidget'; + +const SearchPage: React.FC = () => { + return ( + <> + + + ); +}; + +export default SearchPage; diff --git a/mtucijobsweb/fsd/widgets/Resume/Resume.module.scss b/mtucijobsweb/fsd/widgets/Resume/Resume.module.scss new file mode 100644 index 0000000..5a05721 --- /dev/null +++ b/mtucijobsweb/fsd/widgets/Resume/Resume.module.scss @@ -0,0 +1,39 @@ +.form { + font-size: 28px; + color: white; +} +// .inputs{ +// background-color: var(--secondary_bg_color), +// } +.checkbox_button { + color:white; +} + +// .checkbox_button:hover { +// border-color: #40a9ff; +// } + +// .checkbox_button input { +// display: none; +// } + +// .checkbox_button-checked { +// background-color: #40a9ff; +// color: white; +// border-color: #40a9ff; +// } + +// .checkbox_button-checked:hover { +// background-color: #69c0ff; +// border-color: #69c0ff; +// } + +// .checkbox_button-checked .ant-checkbox-inner { +// background-color: transparent; +// } + +// .checkbox_button input:checked + .checkbox_button { +// background-color: #40a9ff; +// color: white; +// border-color: #40a9ff; +// } diff --git a/mtucijobsweb/fsd/widgets/Resume/Resume.tsx b/mtucijobsweb/fsd/widgets/Resume/Resume.tsx new file mode 100644 index 0000000..0bec209 --- /dev/null +++ b/mtucijobsweb/fsd/widgets/Resume/Resume.tsx @@ -0,0 +1,403 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Checkbox, Form, Input, Radio, Select } from 'antd'; +import r from './Resume.module.scss'; +import { initThemeParams, MiniApp, useInitData, useMiniApp, usePopup } from '@tma.js/sdk-react'; +import { + deleteStudent, + editStudent, + fetchHardSkills, + fetchHardSkillsAll, + fetchStudent, + sendStudent, +} from '@/api/api'; +import { Bot, FormValues, Request, Student } from '@/types/types'; +import { useQuery } from 'react-query'; +import { sendDataBot } from '@/api/bot'; + +type LayoutType = Parameters[0]['layout']; +const { Option } = Select; +const Resume: React.FC = () => { + const [form] = Form.useForm(); + const [formLayout, setFormLayout] = useState('horizontal'); + const miniApp = useMiniApp(); + const [themeParams] = initThemeParams(); + const initData = useInitData(true); + const popup = usePopup(); + + const { isLoading, isError, data, error } = useQuery( + ['student', initData?.user?.id], + () => fetchStudent(initData?.user?.id || 0), + { + enabled: !!initData?.user?.id, // Включить запрос только когда initData.user.id доступен + refetchOnWindowFocus: false, + retry: false, + } + ); + const hardSkill = useQuery( + ['skills', initData?.user?.id], + () => fetchHardSkills(initData?.user?.id || 0), + { + enabled: !!initData?.user?.id, // Включить запрос только когда initData.user.id доступен + refetchOnWindowFocus: false, + retry: false, + } + ); + + + const { data: allSkills } = useQuery( + ['hardSkills'], // Обратите внимание на использование массива в качестве ключа + () => fetchHardSkillsAll(), + { + refetchOnWindowFocus: false, + retry: false, + } + ); + + + const onFinish = async (values: FormValues) => { + const hard: string[] = values.skills; + const student: Request = { + StudentID: initData?.user?.id || 0, + Name: values.Name, + Type: values.Type, + Group: values.Group, + Faculties: values.Faculties, + Phone_number: values.Phone_number, + Time: values.Time, // список времени + Soft_skills: values.Soft_skills, + Link: 'link', // предполагается, что у вас есть ссылка + Email: values.Email, + Hardskills: values.skills, // список навыков + }; + const editedStudent: Omit = { + Name: values.Name, + Type: values.Type, + Group: values.Group, + Time: values.Time, + Faculties: values.Faculties, + Phone_number: values.Phone_number, + Soft_skills: values.Soft_skills, + Link: 'link', + Email: values.Email, + Hardskills: values.skills, + }; + const dataBot: Bot = { + id: initData?.user?.id || 0, + }; + if (data) { + const result = await editStudent(editedStudent, initData?.user?.id || 0); + if (result.status == 200) { + popup.open({ + title: '', + message: 'Изменния были внесены', + buttons: [{ id: 'my-id', type: 'default', text: 'Закрыть' }], + }); + } + console.log(result.status); + } else { + const result = await sendStudent(student); + console.log(result.status); + const resultbot = await sendDataBot(dataBot); + console.log(resultbot); + miniApp.close(); + } + }; + + useEffect(() => { + if (data) { + form.setFieldsValue({ + Name: data.data.Name, + Type: data.data.Type, + Group: data.data.Group, + Time: data.data.Time, + Soft_skills: data.data.Soft_skills, + Email: data.data.Email, + skills: hardSkill.data?.data.map( + (skill: { Title: string }) => skill.Title + ), + }); + } + }, [data, form]); + + const resetResume = async () => { + const result = await deleteStudent(initData?.user?.id || 0); + if (result.status == 204) { + popup.open({ + title: '', + message: 'Ваше резюме было удалено', + buttons: [{ id: 'my-id', type: 'default', text: 'Закрыть' }], + }); + } + await form.resetFields(); + miniApp.close(); + }; + + const [selectedValues, setSelectedValues] = useState([]); + + const handleCheckboxChange = (checkedValues: string[]) => { + setSelectedValues(checkedValues); + }; + + const onFormLayoutChange = ({ layout }: { layout: LayoutType }) => { + setFormLayout(layout); + }; + + // if (isLoading) { + // return Загрузка...; + // } + + return ( +
+ + Как вас зовут? + + } + className={r.form_item} + rules={[ + { required: true, message: 'Пожалуйста, введите имя и фамилию!' }, + ]} + name='Name' + > + + + + Факультет + + } + className={r.form_item} + rules={[{ required: true, message: 'Пожалуйста, введите факультет!' }]} + name='Faculties' + > + + + + Номер телефона + + } + className={r.form_item} + rules={[ + { required: true, message: 'Пожалуйста, введите номер телефона!' }, + ]} + name='Phone_number' + > + + + + + Что вы ищите? + + } + rules={[ + { required: true, message: 'Пожалуйста, выберит тип занятости!' }, + ]} + name='Type' + > + + Работу + Стажировку + + + + {/* + Что вы ищите? + + } + rules={[ + { required: true, message: 'Пожалуйста, выберите тип занятости!' }, + ]} + name='Type' + > + + */} + + + Ваша академическая группа: + + } + name='Group' + rules={[ + { + required: true, + message: 'Пожалуйста, введите номер вашей группы!', + }, + { + pattern: /^[А-ЯЁ]{2,3}\d{4}$/, + message: 'Введите корректный номер группы (например, БВТ2202)!', + }, + ]} + > + + + + + Какую занятость (часов в неделю) вы рассматриваете? + + } + name='Time' + > + + + 20 + + + 30 + + + 40 + + + + + Какими навыками вы обладаете? + + } + rules={[ + { required: true, message: 'Пожалуйста, укажите ваши hard skills!' }, + ]} + name='skills' + > + + + + + Расскажите немного о своих soft skills: + + } + rules={[ + { required: true, message: 'Пожалуйста, введите ваши soft skills!' }, + ]} + name='Soft_skills' + > + + + + + Оставьте почту для работодателей: + + } + className={r.form_item} + rules={[ + { required: true, message: 'Пожалуйста, введите вашу почту!' }, + { type: 'email', message: 'Введите корректный email!' }, + ]} + name='Email' + > + + + + + + + {data && ( + + + + )} +
+ ); +}; + +export default Resume; diff --git a/mtucijobsweb/fsd/widgets/Search/SearchWidget.module.scss b/mtucijobsweb/fsd/widgets/Search/SearchWidget.module.scss new file mode 100644 index 0000000..eb7167f --- /dev/null +++ b/mtucijobsweb/fsd/widgets/Search/SearchWidget.module.scss @@ -0,0 +1,38 @@ +.container { + padding: 20px; +} + +.form { + margin-bottom: 20px; + color: white; +} + +.error { + color: red; +} + +.item{ + color:white; +} +.spin{ + position:absolute; + left:50%; + top:50%; + transform: translate(-50%, -50%); +} +.cardWrapper { + display: flex; + flex-direction: column; + gap: 20px; /* Расстояние между карточками */ +} + +.card { + border: 1px solid #d9d9d9; /* Цвет и стиль обводки */ + border-radius: 4px; /* Закругление углов */ + padding: 16px; /* Отступы внутри карточки */ + transition: box-shadow 0.3s; /* Плавный переход для теней */ + + &:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Тень при наведении */ + } +} \ No newline at end of file diff --git a/mtucijobsweb/fsd/widgets/Search/SearchWidget.tsx b/mtucijobsweb/fsd/widgets/Search/SearchWidget.tsx new file mode 100644 index 0000000..4499a5b --- /dev/null +++ b/mtucijobsweb/fsd/widgets/Search/SearchWidget.tsx @@ -0,0 +1,175 @@ +'use client' +import React, { useState, useEffect } from 'react'; +import { useQuery } from 'react-query'; +import { searchJobs } from '@/api/api'; +import { Input, Button, Spin, List, Select, Checkbox, InputNumber, Card } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import style from './SearchWidget.module.scss'; +import { JobData } from '@/types/types'; +import { skillsOptions } from '@/fsd/entities/Resume/data'; +import { initThemeParams } from '@tma.js/sdk-react'; + +const { Option } = Select; + +const SearchWidget: React.FC = () => { + const [query, setQuery] = useState(''); + const [year, setYear] = useState(); + const [qualification, setQualification] = useState(); + const [time, setTime] = useState([]); + const [salary, setSalary] = useState(); + const [hardskills, setHardskills] = useState([]); + const [searchResults, setSearchResults] = useState([]); + const [themeParams] = initThemeParams(); + + // Функция для формирования объекта параметров + const buildQueryParams = () => { + const params: any = {}; // Или укажите более точный тип, соответствующий JobsSearch + + if (year !== undefined) { + params.year = year; + } + if (qualification !== undefined) { + params.qualification = qualification; + } + if (time.length > 0) { + params.time = time; // здесь может потребоваться изменить на массив строк + } + if (salary !== undefined) { + params.salary = salary; + } + if (hardskills.length > 0) { + params.hardskills = hardskills; + } + if (query) { + params.search = query; + } + + return params; // Возвращаем объект вместо строки + }; + + // Вызов useQuery с объектом + const { data, error, isLoading, refetch } = useQuery( + ['searchJobs', buildQueryParams()], + () => { + const queryParams = buildQueryParams(); // Получаем объект параметров + return searchJobs(queryParams); // Передаём объект в функцию + }, + { enabled: false, refetchOnWindowFocus: false, retry: false } + ); + + // Используем useEffect для обновления searchResults, когда запрос завершен + useEffect(() => { + if (data) { + setSearchResults(data); + } + }, [data]); + + const handleSearch = () => { + refetch(); + }; + + if (isLoading) return ; + if (error) + return
Ошибка при поиске вакансий
; + + return ( +
+
+ setQuery(e.target.value)} + prefix={} + style={{ marginBottom: 20 }} + /> + + + + setQualification(e.target.checked)} + style={{ marginBottom: 20, color: themeParams.textColor }} + > + Требуется квалификация + + + + + setSalary(value !== null ? value : undefined)} + style={{ width: '100%', marginBottom: 20 }} + /> + + + + + + + + + + + + +
+ ); +}; + +export default LoginComponent; diff --git a/mtucijobsweb2/fsd/widgets/Redirect/Redirect.tsx b/mtucijobsweb2/fsd/widgets/Redirect/Redirect.tsx new file mode 100644 index 0000000..14e89d9 --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/Redirect/Redirect.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + + +const Redirect = () => { + const router = useRouter(); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + router.push('/create'); + } else { + router.replace('/login'); + } + }, [router]); + + return null; +} + +export default Redirect \ No newline at end of file diff --git a/mtucijobsweb2/fsd/widgets/Resume/Resume.module.scss b/mtucijobsweb2/fsd/widgets/Resume/Resume.module.scss new file mode 100644 index 0000000..7a2aeb7 --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/Resume/Resume.module.scss @@ -0,0 +1,13 @@ +.not_found { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} +.card_wrapper { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + margin-top:100px; + margin-left:100px; +} diff --git a/mtucijobsweb2/fsd/widgets/Resume/Resume.tsx b/mtucijobsweb2/fsd/widgets/Resume/Resume.tsx new file mode 100644 index 0000000..76ebbb5 --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/Resume/Resume.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { ResumeData } from '@/types/types'; +import { useQuery } from 'react-query'; +import React from 'react'; +import { Card, Spin, Button } from 'antd'; +import style from './Resume.module.scss'; +import { + fetchJobsMatches, + fetchJobsHardSkills, + downloadResume, +} from '@/api/api'; + +interface ResumeProps { + id: number; +} + +const Resume: React.FC = ({ id }) => { + // Получение данных о резюме + const { + data: resumeData, + error: resumeError, + isLoading: resumeLoading, + } = useQuery(['matches', id], () => fetchJobsMatches(id), { + refetchOnWindowFocus: false, + retry: false, + }); + + // Получение данных о хардскиллах + const { + data: hardSkillsData, + error: hardSkillsError, + isLoading: hardSkillsLoading, + } = useQuery(['hardSkills', id], () => fetchJobsHardSkills(id), { + refetchOnWindowFocus: false, + retry: false, + }); + + if (resumeLoading || hardSkillsLoading) return ; + if (resumeError || hardSkillsError) + return ( +
+ Произошла ошибка при загрузке данных +
+ ); + + // Функция для обработки клика по кнопке скачивания + const handleDownload = (filename: string) => { + downloadResume(filename); + }; + + + console.log(resumeData) + return ( +
+ {resumeData && resumeData.length > 0 ? ( +
+ {resumeData.map((resume, index) => ( + +

+ Тип: {resume.Type} +

+

+ Группа: {resume.Group} +

+

+ Занятость:{' '} + {resume.Time.length > 1 + ? resume.Time.join(', ') + : resume.Time[0]} +

+

+ Soft Skills: {resume.Soft_skills} +

+

+ Номер телефона: {resume.Phone_number} +

+

+ Факультет: {resume.Faculties} +

+

+ Email: {resume.Email} +

+ {hardSkillsData && hardSkillsData.length > 0 && ( +

+ Hard Skills: {hardSkillsData.join(', ')} +

+ )} + +
+ ))} +
+ ) : ( +
Подходящих резюме не найдено
+ )} +
+ ); +}; + +export default Resume; diff --git a/mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.module.scss b/mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.module.scss new file mode 100644 index 0000000..798fec7 --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.module.scss @@ -0,0 +1,51 @@ +.filter_section { + display: flex; + gap: 10px; + margin-bottom: 20px; + +} + +.form{ + width:500px; + margin-top:50px; + position:relative; + left:50%; + transform: translate(-50%); +} + +.card_wrapper { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + margin: 100px; +} +.search_button{ + background-color: #372579; + &:hover{ + background: #47309C !important; + } +} + +.spin{ + position:absolute; + left:50%; + top:50%; + transform: translate(-50%, -50%); +} +.empty{ + color:black; + position:absolute; + left:50%; + transform: translateX( -50%); + font-size:20px; +} + + + +@media screen and (max-width: 580px) { +.form{ + width:350px; + + +} +} diff --git a/mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.tsx b/mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.tsx new file mode 100644 index 0000000..897381a --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.tsx @@ -0,0 +1,149 @@ +'use client' +import React, { useState, useEffect } from 'react'; +import { useQuery } from 'react-query'; +import { fetchHardSkills, searchStudents } from '@/api/api'; +import { Input, Button, Spin, List, Select, Card } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import style from './SearchResume.module.scss'; +import { ResumeDataWithoutSkills } from '@/types/types'; + +const { Option } = Select; + +const SearchResume: React.FC = () => { + const [year, setYear] = useState(); + const [time, setTime] = useState([]); + const [hardskills, setHardskills] = useState([]); + const [searchResults, setSearchResults] = useState< + ResumeDataWithoutSkills[] | null + >(null); + + // Функция для формирования строки запроса + const buildQueryParams = () => { + const params = new URLSearchParams(); + + if (year !== undefined) { + params.append('year', year.toString()); + } + + if (time.length > 0) { + time.forEach(t => params.append('time', t)); + } + + if (hardskills.length > 0) { + hardskills.forEach(skill => params.append('hardskills', skill)); + } + + return params.toString(); + }; + + const { data, error, isLoading, refetch } = useQuery< + ResumeDataWithoutSkills[] + >( + ['searchStudents', buildQueryParams()], + () => { + const queryString = buildQueryParams(); + return searchStudents(queryString); + }, + { enabled: false, refetchOnWindowFocus: false, retry: false } + ); + + const { data: hardSkills } = useQuery( + ['hardSkills'], + () => fetchHardSkills(), + { + refetchOnWindowFocus: false, + retry: false, + } + ); + + // Используем useEffect для обновления searchResults, когда запрос завершен + useEffect(() => { + if (data) { + setSearchResults(data); + } + }, [data]); + + const handleSearch = () => { + refetch(); + }; + + return ( +
+
+ + + + + + + +
+ +
+ {isLoading && } + {searchResults && searchResults.length > 0 ? ( +
+ {searchResults.map((item: ResumeDataWithoutSkills) => ( + +

Тип: {item.Type}

+

Группа: {item.Group}

+

Занятость: {item.Time.join(', ')}

+

Soft Skills: {item.Soft_skills}

+

Номер телефона: {item.Phone_number}

+

Факультет: {item.Faculties}

+

+ Ссылка: {item.Link} +

+

Email: {item.Email}

+
+ ))} +
+ ) : ( + !isLoading &&
Резюме не найдены
+ )} +
+
+ ); +}; + +export default SearchResume; diff --git a/mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.module.scss b/mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.module.scss new file mode 100644 index 0000000..8d8658b --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.module.scss @@ -0,0 +1,25 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width:100%; + // background-color: #f0f2f5; /* Легкий серый фон для контраста */ +} + +.form { + width: 400px; /* Ширина формы */ + padding: 24px; /* Внутренние отступы */ + background: white; /* Белый фон формы */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Легкая тень */ + border-radius: 8px; /* Скругленные углы */ + margin-top:50px; + margin-bottom: 50px; +} + +.vacansy_btn{ + background-color: #372579; + &:hover{ + background: #47309C !important; + } +} \ No newline at end of file diff --git a/mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx b/mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx new file mode 100644 index 0000000..75f459c --- /dev/null +++ b/mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx @@ -0,0 +1,211 @@ +'use client'; +import React, { useState } from 'react'; +import { + Form, + Input, + Button, + Checkbox, + InputNumber, + Select, + notification, +} from 'antd'; +import { useForm } from 'antd/es/form/Form'; +import styles from './Vacansy.module.scss'; +import { JobData } from '@/types/types'; +import { fetchHardSkills, sendJobs } from '@/api/api'; +import { useQuery } from 'react-query'; + +const { TextArea } = Input; +const { Option } = Select; + +const Vacansy: React.FC = () => { + const [form] = useForm(); + const [isSalaryNegotiable, setIsSalaryNegotiable] = useState(false); // Состояние для чекбокса + + const { data: hardSkills, isLoading } = useQuery( + ['hardSkills'], + () => fetchHardSkills(), + { + refetchOnWindowFocus: false, + retry: false, + } + ); + + const onFinish = async (values: JobData) => { + const jobData: JobData = { + Job_name: values.Job_name, + Year: values.Year, + Qualification: values.Qualification || false, + Time: Array.isArray(values.Time) ? values.Time : [values.Time], + Company_name: values.Company_name, + Salary: isSalaryNegotiable ? 0 : values.Salary, // Установка зарплаты в 0, если выбрана опция "уточняется" + Email: values.Email, + // Website: values.Website, // Новое поле для ссылки на сайт компании + Archive: false, + Responsibilities: values.Responsibilities, + Hardskills: values.Hardskills, + }; + + try { + const result = await sendJobs(jobData); + console.log('Job successfully posted:', result); + + notification.success({ + message: 'Вакансия успешно создана', + description: 'Вы можете посмотреть её на странице вакансий', + }); + await form.resetFields(); + setIsSalaryNegotiable(false); // Сброс чекбокса + } catch (error) { + console.error('Failed to post job:', error); + notification.error({ + message: 'Что-то пошло не так!', + description: 'Попробуйте снова', + }); + } + }; + + return ( +
+
+ + + + + + + + + + Нужен ли опыт работы? + + + + + + + + + + + + setIsSalaryNegotiable(e.target.checked)}> + Зарплата уточняется после собеседования + + + + + + + + + + + + + + + + +