copied the code from the working repo
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.env
|
||||||
33
Jenkinsfile
vendored
Normal file
33
Jenkinsfile
vendored
Normal file
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
compose.https.yml
Normal file
28
compose.https.yml
Normal file
@@ -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
|
||||||
181
compose.yml
Normal file
181
compose.yml
Normal file
@@ -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
|
||||||
5
mtucijobsbackend/.env.example
Normal file
5
mtucijobsbackend/.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
database_hostname =
|
||||||
|
database_port =
|
||||||
|
database_password =
|
||||||
|
database_name =
|
||||||
|
database_username =
|
||||||
3
mtucijobsbackend/.gitignore
vendored
Normal file
3
mtucijobsbackend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
6
mtucijobsbackend/Dockerfile
Normal file
6
mtucijobsbackend/Dockerfile
Normal file
@@ -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/"]
|
||||||
0
mtucijobsbackend/app/__init__.py
Normal file
0
mtucijobsbackend/app/__init__.py
Normal file
21
mtucijobsbackend/app/config.py
Normal file
21
mtucijobsbackend/app/config.py
Normal file
@@ -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()
|
||||||
150
mtucijobsbackend/app/crud.py
Normal file
150
mtucijobsbackend/app/crud.py
Normal file
@@ -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)
|
||||||
|
|
||||||
47
mtucijobsbackend/app/database.py
Normal file
47
mtucijobsbackend/app/database.py
Normal file
@@ -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()
|
||||||
32
mtucijobsbackend/app/main.py
Normal file
32
mtucijobsbackend/app/main.py
Normal file
@@ -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!"}
|
||||||
87
mtucijobsbackend/app/models.py
Normal file
87
mtucijobsbackend/app/models.py
Normal file
@@ -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)
|
||||||
51
mtucijobsbackend/app/routers/auth.py
Normal file
51
mtucijobsbackend/app/routers/auth.py
Normal file
@@ -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}
|
||||||
247
mtucijobsbackend/app/routers/job.py
Normal file
247
mtucijobsbackend/app/routers/job.py
Normal file
@@ -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)
|
||||||
55
mtucijobsbackend/app/routers/service.py
Normal file
55
mtucijobsbackend/app/routers/service.py
Normal file
@@ -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}
|
||||||
197
mtucijobsbackend/app/routers/student.py
Normal file
197
mtucijobsbackend/app/routers/student.py
Normal file
@@ -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)
|
||||||
60
mtucijobsbackend/app/routers/user.py
Normal file
60
mtucijobsbackend/app/routers/user.py
Normal file
@@ -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
|
||||||
146
mtucijobsbackend/app/schemas.py
Normal file
146
mtucijobsbackend/app/schemas.py
Normal file
@@ -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
|
||||||
90
mtucijobsbackend/app/security.py
Normal file
90
mtucijobsbackend/app/security.py
Normal file
@@ -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
|
||||||
21
mtucijobsbackend/app/storage.py
Normal file
21
mtucijobsbackend/app/storage.py
Normal file
@@ -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)
|
||||||
8
mtucijobsbackend/app/utils.py
Normal file
8
mtucijobsbackend/app/utils.py
Normal file
@@ -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)
|
||||||
61
mtucijobsbackend/requirements.txt
Normal file
61
mtucijobsbackend/requirements.txt
Normal file
@@ -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
|
||||||
0
mtucijobsbackend/tests/__init__.py
Normal file
0
mtucijobsbackend/tests/__init__.py
Normal file
36
mtucijobsbackend/tests/conftest.py
Normal file
36
mtucijobsbackend/tests/conftest.py
Normal file
@@ -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)
|
||||||
158
mtucijobsbackend/tests/test_students.py
Normal file
158
mtucijobsbackend/tests/test_students.py
Normal file
@@ -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
|
||||||
13
mtucijobsbot/.dockerignore
Normal file
13
mtucijobsbot/.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
**/node_modules
|
||||||
|
*.md
|
||||||
|
*.env
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
**/.npmignore
|
||||||
|
**/.dockerignore
|
||||||
|
**/*.md
|
||||||
|
**/*.log
|
||||||
|
**/.vscode
|
||||||
|
**/.git
|
||||||
|
**/.eslintrc.json
|
||||||
|
*.sh
|
||||||
9
mtucijobsbot/.env.development
Normal file
9
mtucijobsbot/.env.development
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
BOT_TOKEN=
|
||||||
|
DB_NAME=
|
||||||
|
DB_HOST=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
PORT=
|
||||||
|
HOOKPORT=
|
||||||
|
DOMAIN=
|
||||||
|
WEB_APP=
|
||||||
4
mtucijobsbot/.gitignore
vendored
Normal file
4
mtucijobsbot/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.DS_store
|
||||||
|
**/.DS_Store
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
12
mtucijobsbot/Dockerfile
Normal file
12
mtucijobsbot/Dockerfile
Normal file
@@ -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"]
|
||||||
5
mtucijobsbot/build.sh
Normal file
5
mtucijobsbot/build.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -f Dockerfile . \
|
||||||
|
-t mtuci-jobs-image:latest \
|
||||||
|
--compress \
|
||||||
|
--force-rm
|
||||||
5
mtucijobsbot/deploy.sh
Normal file
5
mtucijobsbot/deploy.sh
Normal file
@@ -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
|
||||||
40
mtucijobsbot/dist/api/resume.js
vendored
Normal file
40
mtucijobsbot/dist/api/resume.js
vendored
Normal file
@@ -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;
|
||||||
BIN
mtucijobsbot/dist/assets/шаблон МТУСИ.docx
vendored
Normal file
BIN
mtucijobsbot/dist/assets/шаблон МТУСИ.docx
vendored
Normal file
Binary file not shown.
90
mtucijobsbot/dist/commands/start.js
vendored
Normal file
90
mtucijobsbot/dist/commands/start.js
vendored
Normal file
@@ -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;
|
||||||
109
mtucijobsbot/dist/db/db.js
vendored
Normal file
109
mtucijobsbot/dist/db/db.js
vendored
Normal file
@@ -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);
|
||||||
|
});
|
||||||
375
mtucijobsbot/dist/index.js
vendored
Normal file
375
mtucijobsbot/dist/index.js
vendored
Normal file
@@ -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<MyWizardContext> = 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, `<b>${data.Job_name}</b>\n\n` +
|
||||||
|
`<b>Компания:</b> ${data.Company_name}\n` +
|
||||||
|
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
|
||||||
|
`<b>Контактные данные:</b> ${data.Email}\n\n` +
|
||||||
|
`<b>Требования к кандидату:</b>\n` +
|
||||||
|
` - ${data.Year} курс\n` +
|
||||||
|
` - Опыт работы по специальности: ${data.Qualification}\n` +
|
||||||
|
` - Soft skills: ${data.Soft_skills}\n` +
|
||||||
|
`<b>Обязанности:</b>\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}`);
|
||||||
|
});
|
||||||
24
mtucijobsbot/dist/languages/en.json
vendored
Normal file
24
mtucijobsbot/dist/languages/en.json
vendored
Normal file
@@ -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" }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
mtucijobsbot/dist/languages/ru.json
vendored
Normal file
24
mtucijobsbot/dist/languages/ru.json
vendored
Normal file
@@ -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" }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
11
mtucijobsbot/dist/models/User.js
vendored
Normal file
11
mtucijobsbot/dist/models/User.js
vendored
Normal file
@@ -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 = {}));
|
||||||
50
mtucijobsbot/dist/modules/menuController.js
vendored
Normal file
50
mtucijobsbot/dist/modules/menuController.js
vendored
Normal file
@@ -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;
|
||||||
38
mtucijobsbot/dist/modules/menuScenes.js
vendored
Normal file
38
mtucijobsbot/dist/modules/menuScenes.js
vendored
Normal file
@@ -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;
|
||||||
112
mtucijobsbot/dist/modules/scenes/accept.js
vendored
Normal file
112
mtucijobsbot/dist/modules/scenes/accept.js
vendored
Normal file
@@ -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;
|
||||||
292
mtucijobsbot/dist/modules/scenes/events.js
vendored
Normal file
292
mtucijobsbot/dist/modules/scenes/events.js
vendored
Normal file
@@ -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(`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${data.Group}\n<b>Тип:</b> ${data.Type}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${data.Soft_skills}\n<b>Почта:</b> ${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(`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${data.Group}\n<b>Тип:</b> ${data.Type}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${data.Soft_skills}\n<b>Почта:</b> ${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(`<b>${data.Job_name}</b>\n\n` +
|
||||||
|
`<b>Компания:</b> ${data.Company_name}\n` +
|
||||||
|
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
|
||||||
|
`<b>Контактные данные:</b> ${data.Email}\n\n` +
|
||||||
|
`<b>Требования к кандидату:</b>\n` +
|
||||||
|
` - ${data.Year} курс\n` +
|
||||||
|
` - Опыт работы по специальности: ${data.Qualification}\n` +
|
||||||
|
` - Soft skills: ${data.Soft_skills}\n` +
|
||||||
|
`<b>Обязанности:</b>\n` +
|
||||||
|
`${data.Responsibilities}`, {
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: inlineKeyboard,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.events = events;
|
||||||
39
mtucijobsbot/dist/modules/scenes/invest.js
vendored
Normal file
39
mtucijobsbot/dist/modules/scenes/invest.js
vendored
Normal file
@@ -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<Scenes.WizardContext>) {
|
||||||
|
// 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<MyWizardContext> =
|
||||||
|
// 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();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
55
mtucijobsbot/dist/modules/scenes/savevacansy.js
vendored
Normal file
55
mtucijobsbot/dist/modules/scenes/savevacansy.js
vendored
Normal file
@@ -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;
|
||||||
277
mtucijobsbot/dist/services/messagesService.js
vendored
Normal file
277
mtucijobsbot/dist/services/messagesService.js
vendored
Normal file
@@ -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 = {};
|
||||||
62
mtucijobsbot/docker-compose.yml
Normal file
62
mtucijobsbot/docker-compose.yml
Normal file
@@ -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
|
||||||
3007
mtucijobsbot/package-lock.json
generated
Normal file
3007
mtucijobsbot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
mtucijobsbot/package.json
Normal file
37
mtucijobsbot/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
mtucijobsbot/run.sh
Normal file
14
mtucijobsbot/run.sh
Normal file
@@ -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
|
||||||
34
mtucijobsbot/src/api/resume.ts
Normal file
34
mtucijobsbot/src/api/resume.ts
Normal file
@@ -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;
|
||||||
BIN
mtucijobsbot/src/assets/шаблон МТУСИ.docx
Normal file
BIN
mtucijobsbot/src/assets/шаблон МТУСИ.docx
Normal file
Binary file not shown.
90
mtucijobsbot/src/commands/start.ts
Normal file
90
mtucijobsbot/src/commands/start.ts
Normal file
@@ -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<void> {
|
||||||
|
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('Произошла ошибка при запуске. Пожалуйста, попробуйте позже.');
|
||||||
|
}
|
||||||
|
}
|
||||||
176
mtucijobsbot/src/db/db.ts
Normal file
176
mtucijobsbot/src/db/db.ts
Normal file
@@ -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<UserAttributes, UserCreationAttributes>
|
||||||
|
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<ResumeAttributes, ResumeCreationAttributes>
|
||||||
|
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);
|
||||||
|
});
|
||||||
469
mtucijobsbot/src/index.ts
Normal file
469
mtucijobsbot/src/index.ts
Normal file
@@ -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<Scenes.WizardSessionData>;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
bot: Telegraf<Scenes.WizardContext>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bot = new Telegraf<Scenes.WizardContext>(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<MyWizardContext> = 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<MyWizardContext> = 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,
|
||||||
|
`<b>${data.Job_name}</b>\n\n` +
|
||||||
|
`<b>Компания:</b> ${data.Company_name}\n` +
|
||||||
|
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
|
||||||
|
`<b>Контактные данные:</b> ${data.Email}\n\n` +
|
||||||
|
`<b>Требования к кандидату:</b>\n` +
|
||||||
|
` - ${data.Year} курс\n` +
|
||||||
|
` - Опыт работы по специальности: ${data.Qualification}\n` +
|
||||||
|
` - Soft skills: ${data.Soft_skills}\n` +
|
||||||
|
`<b>Обязанности:</b>\n` +
|
||||||
|
`${data.Responsibilities}`,
|
||||||
|
{
|
||||||
|
parse_mode: 'HTML', // Используем HTML для форматирования текста
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Сохранить вакансию',
|
||||||
|
callback_data: `savevacancy+${data.JobID}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик для получения файла резюме
|
||||||
|
|
||||||
|
const stage = new Scenes.Stage<MyWizardContext>([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}`);
|
||||||
|
});
|
||||||
24
mtucijobsbot/src/languages/en.json
Normal file
24
mtucijobsbot/src/languages/en.json
Normal file
@@ -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" }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
mtucijobsbot/src/languages/ru.json
Normal file
24
mtucijobsbot/src/languages/ru.json
Normal file
@@ -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" }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
23
mtucijobsbot/src/models/User.ts
Normal file
23
mtucijobsbot/src/models/User.ts
Normal file
@@ -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',
|
||||||
|
}
|
||||||
43
mtucijobsbot/src/modules/menuController.ts
Normal file
43
mtucijobsbot/src/modules/menuController.ts
Normal file
@@ -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<void> {
|
||||||
|
// await MessagesService.sendMessage(ctx, 'greeting');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setLanguage(ctx: Context): Promise<void> {
|
||||||
|
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(
|
||||||
|
'Произошла ошибка при обновлении языка. Пожалуйста, попробуйте снова позже.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
mtucijobsbot/src/modules/menuScenes.ts
Normal file
39
mtucijobsbot/src/modules/menuScenes.ts
Normal file
@@ -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<Scenes.WizardContext> = 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;
|
||||||
107
mtucijobsbot/src/modules/scenes/accept.ts
Normal file
107
mtucijobsbot/src/modules/scenes/accept.ts
Normal file
@@ -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<Scenes.WizardContext>) {
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
}
|
||||||
366
mtucijobsbot/src/modules/scenes/events.ts
Normal file
366
mtucijobsbot/src/modules/scenes/events.ts
Normal file
@@ -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<Scenes.WizardContext>) {
|
||||||
|
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(
|
||||||
|
`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${
|
||||||
|
data.Group
|
||||||
|
}\n<b>Тип:</b> ${
|
||||||
|
data.Type
|
||||||
|
}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${
|
||||||
|
data.Soft_skills
|
||||||
|
}\n<b>Почта:</b> ${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(
|
||||||
|
`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${
|
||||||
|
data.Group
|
||||||
|
}\n<b>Тип:</b> ${
|
||||||
|
data.Type
|
||||||
|
}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${
|
||||||
|
data.Soft_skills
|
||||||
|
}\n<b>Почта:</b> ${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(
|
||||||
|
`<b>${data.Job_name}</b>\n\n` +
|
||||||
|
`<b>Компания:</b> ${data.Company_name}\n` +
|
||||||
|
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
|
||||||
|
`<b>Контактные данные:</b> ${data.Email}\n\n` +
|
||||||
|
`<b>Требования к кандидату:</b>\n` +
|
||||||
|
` - ${data.Year} курс\n` +
|
||||||
|
` - Опыт работы по специальности: ${data.Qualification}\n` +
|
||||||
|
` - Soft skills: ${data.Soft_skills}\n` +
|
||||||
|
`<b>Обязанности:</b>\n` +
|
||||||
|
`${data.Responsibilities}`,
|
||||||
|
{
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: inlineKeyboard,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
mtucijobsbot/src/modules/scenes/savevacansy.ts
Normal file
49
mtucijobsbot/src/modules/scenes/savevacansy.ts
Normal file
@@ -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<Scenes.WizardContext>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
371
mtucijobsbot/src/services/messagesService.ts
Normal file
371
mtucijobsbot/src/services/messagesService.ts
Normal file
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
mtucijobsbot/tsconfig.json
Normal file
109
mtucijobsbot/tsconfig.json
Normal file
@@ -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 '<reference>'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. */
|
||||||
|
}
|
||||||
|
}
|
||||||
12
mtucijobsweb/.dockerignore
Normal file
12
mtucijobsweb/.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
**/node_modules
|
||||||
|
*.md
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
**/.npmignore
|
||||||
|
**/.dockerignore
|
||||||
|
**/*.md
|
||||||
|
**/*.log
|
||||||
|
**/.vscode
|
||||||
|
**/.git
|
||||||
|
**/.eslintrc.json
|
||||||
|
*.sh
|
||||||
4
mtucijobsweb/.eslintrc.js
Normal file
4
mtucijobsweb/.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['next', 'next/core-web-vitals'],
|
||||||
|
};
|
||||||
35
mtucijobsweb/.gitignore
vendored
Normal file
35
mtucijobsweb/.gitignore
vendored
Normal file
@@ -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
|
||||||
18
mtucijobsweb/Dockerfile
Normal file
18
mtucijobsweb/Dockerfile
Normal file
@@ -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"]
|
||||||
36
mtucijobsweb/README.md
Normal file
36
mtucijobsweb/README.md
Normal file
@@ -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.
|
||||||
77
mtucijobsweb/api/api.ts
Normal file
77
mtucijobsweb/api/api.ts
Normal file
@@ -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<Student, 'StudentID'>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
10
mtucijobsweb/api/axiosInstance.ts
Normal file
10
mtucijobsweb/api/axiosInstance.ts
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
15
mtucijobsweb/api/bot.ts
Normal file
15
mtucijobsweb/api/bot.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
27
mtucijobsweb/app/layout.tsx
Normal file
27
mtucijobsweb/app/layout.tsx
Normal file
@@ -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 (
|
||||||
|
<TmaSDKLoader>
|
||||||
|
<ClientProvider>
|
||||||
|
<html lang='en'>
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
</ClientProvider>
|
||||||
|
</TmaSDKLoader>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
mtucijobsweb/app/page.tsx
Normal file
20
mtucijobsweb/app/page.tsx
Normal file
@@ -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 ? (
|
||||||
|
<MainPage id={initData?.user?.id} />
|
||||||
|
) : (
|
||||||
|
<LoadingPage />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
mtucijobsweb/app/search/page.tsx
Normal file
15
mtucijobsweb/app/search/page.tsx
Normal file
@@ -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 ? <SearchPage /> : <LoadingPage />}</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
||||||
7
mtucijobsweb/build.sh
Normal file
7
mtucijobsweb/build.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build . \
|
||||||
|
--build-arg APP_BASE_URL="" \
|
||||||
|
--no-cache \
|
||||||
|
--rm \
|
||||||
|
--pull \
|
||||||
|
-t mtuci-jobs-web-image:latest
|
||||||
14
mtucijobsweb/fsd/app/providers/ClientProvider.tsx
Normal file
14
mtucijobsweb/fsd/app/providers/ClientProvider.tsx
Normal file
@@ -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 (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientProvider;
|
||||||
13
mtucijobsweb/fsd/app/providers/TmaSDKLoader.tsx
Normal file
13
mtucijobsweb/fsd/app/providers/TmaSDKLoader.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { SDKProvider } from '@tma.js/sdk-react';
|
||||||
|
|
||||||
|
|
||||||
|
export function TmaSDKLoader({ children }: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<SDKProvider acceptCustomStyles debug>
|
||||||
|
{children}
|
||||||
|
</SDKProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
mtucijobsweb/fsd/entities/Resume/data.ts
Normal file
109
mtucijobsweb/fsd/entities/Resume/data.ts
Normal file
@@ -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',
|
||||||
|
];
|
||||||
15
mtucijobsweb/fsd/pages/Loading.tsx
Normal file
15
mtucijobsweb/fsd/pages/Loading.tsx
Normal file
@@ -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 (
|
||||||
|
// <Space>
|
||||||
|
// <Spin indicator={<LoadingOutlined style={{ fontSize: 48, position:'absolute', left:'50%', top: '50%', transform:'translate(-50%, -50%)' }} spin />} />
|
||||||
|
// </Space>
|
||||||
|
<h3 style={{position: 'absolute', left:'50%', top:'50%', transform: 'translate(-50%, -50%)', color: 'white'}}>Загрузака...</h3>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingPage;
|
||||||
17
mtucijobsweb/fsd/pages/MainPage.tsx
Normal file
17
mtucijobsweb/fsd/pages/MainPage.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Resume from "../widgets/Resume/Resume";
|
||||||
|
|
||||||
|
|
||||||
|
interface MainPageProps {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
const MainPage: React.FC<MainPageProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<div >
|
||||||
|
<Resume/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainPage;
|
||||||
13
mtucijobsweb/fsd/pages/SearchPage.tsx
Normal file
13
mtucijobsweb/fsd/pages/SearchPage.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import SearchWidget from '../widgets/Search/SearchWidget';
|
||||||
|
|
||||||
|
const SearchPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchWidget />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchPage;
|
||||||
39
mtucijobsweb/fsd/widgets/Resume/Resume.module.scss
Normal file
39
mtucijobsweb/fsd/widgets/Resume/Resume.module.scss
Normal file
@@ -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;
|
||||||
|
// }
|
||||||
403
mtucijobsweb/fsd/widgets/Resume/Resume.tsx
Normal file
403
mtucijobsweb/fsd/widgets/Resume/Resume.tsx
Normal file
@@ -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<typeof Form>[0]['layout'];
|
||||||
|
const { Option } = Select;
|
||||||
|
const Resume: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [formLayout, setFormLayout] = useState<LayoutType>('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<Student, 'StudentID'> = {
|
||||||
|
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<string[]>([]);
|
||||||
|
|
||||||
|
const handleCheckboxChange = (checkedValues: string[]) => {
|
||||||
|
setSelectedValues(checkedValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormLayoutChange = ({ layout }: { layout: LayoutType }) => {
|
||||||
|
setFormLayout(layout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// if (isLoading) {
|
||||||
|
// return <span>Загрузка...</span>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
layout={formLayout}
|
||||||
|
form={form}
|
||||||
|
initialValues={{ layout: formLayout }}
|
||||||
|
onValuesChange={onFormLayoutChange}
|
||||||
|
onFinish={onFinish}
|
||||||
|
style={{ maxWidth: formLayout === 'inline' ? 'none' : 600 }}
|
||||||
|
className={r.form}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
|
||||||
|
Как вас зовут?
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
className={r.form_item}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, введите имя и фамилию!' },
|
||||||
|
]}
|
||||||
|
name='Name'
|
||||||
|
>
|
||||||
|
<Input placeholder='Фамилия Имя' />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
|
||||||
|
Факультет
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
className={r.form_item}
|
||||||
|
rules={[{ required: true, message: 'Пожалуйста, введите факультет!' }]}
|
||||||
|
name='Faculties'
|
||||||
|
>
|
||||||
|
<Input placeholder='Фамилия Имя' />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
|
||||||
|
Номер телефона
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
className={r.form_item}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, введите номер телефона!' },
|
||||||
|
]}
|
||||||
|
name='Phone_number'
|
||||||
|
>
|
||||||
|
<Input placeholder='Фамилия Имя' />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
|
||||||
|
Что вы ищите?
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, выберит тип занятости!' },
|
||||||
|
]}
|
||||||
|
name='Type'
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio.Button value='Работа'>Работу</Radio.Button>
|
||||||
|
<Radio.Button value='Стажировка'>Стажировку</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* <Form.Item
|
||||||
|
label={
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
fontSize: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Что вы ищите?
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, выберите тип занятости!' },
|
||||||
|
]}
|
||||||
|
name='Type'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode='multiple'
|
||||||
|
allowClear
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder='Выберите тип занятости'
|
||||||
|
>
|
||||||
|
<Option value='ищу работу'>Ищу работу</Option>
|
||||||
|
<Option value='ищу стажировку у партнеров'>
|
||||||
|
Ищу стажировку у партнеров
|
||||||
|
</Option>
|
||||||
|
<Option value='ищу стажировку сторонних компаний'>
|
||||||
|
Ищу стажировку сторонних компаний
|
||||||
|
</Option>
|
||||||
|
<Option value='хочу заниматься научной работой'>
|
||||||
|
Хочу заниматься научной работой
|
||||||
|
</Option>
|
||||||
|
<Option value='хочу работать в МТУСИ'>Хочу работать в МТУСИ</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item> */}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
|
||||||
|
Ваша академическая группа:
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
name='Group'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Пожалуйста, введите номер вашей группы!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^[А-ЯЁ]{2,3}\d{4}$/,
|
||||||
|
message: 'Введите корректный номер группы (например, БВТ2202)!',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder='БВТ2202' style={{ width: '85px' }} maxLength={7} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
fontSize: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Какую занятость (часов в неделю) вы рассматриваете?
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
name='Time'
|
||||||
|
>
|
||||||
|
<Checkbox.Group onChange={handleCheckboxChange} value={selectedValues}>
|
||||||
|
<Checkbox
|
||||||
|
value='20'
|
||||||
|
className={r.checkbox_button}
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
20
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox
|
||||||
|
value='30'
|
||||||
|
className={r.checkbox_button}
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
30
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox
|
||||||
|
value='40'
|
||||||
|
className={r.checkbox_button}
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
40
|
||||||
|
</Checkbox>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
fontSize: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Какими навыками вы обладаете?
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, укажите ваши hard skills!' },
|
||||||
|
]}
|
||||||
|
name='skills'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode='multiple'
|
||||||
|
allowClear
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder='Выберите навыки'
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{allSkills?.map(skill => (
|
||||||
|
<Option key={skill.Hard_skillID} value={skill.Title}>
|
||||||
|
{skill.Title}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
color: themeParams.textColor,
|
||||||
|
fontSize: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Расскажите немного о своих soft skills:
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, введите ваши soft skills!' },
|
||||||
|
]}
|
||||||
|
name='Soft_skills'
|
||||||
|
>
|
||||||
|
<Input.TextArea maxLength={150} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
|
||||||
|
Оставьте почту для работодателей:
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
className={r.form_item}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Пожалуйста, введите вашу почту!' },
|
||||||
|
{ type: 'email', message: 'Введите корректный email!' },
|
||||||
|
]}
|
||||||
|
name='Email'
|
||||||
|
>
|
||||||
|
<Input placeholder='example@mtuci.ru' className={r.inputs} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type='primary' htmlType='submit'>
|
||||||
|
Отправить
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
{data && (
|
||||||
|
<Form.Item>
|
||||||
|
<Button danger onClick={resetResume}>
|
||||||
|
Стереть
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Resume;
|
||||||
38
mtucijobsweb/fsd/widgets/Search/SearchWidget.module.scss
Normal file
38
mtucijobsweb/fsd/widgets/Search/SearchWidget.module.scss
Normal file
@@ -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); /* Тень при наведении */
|
||||||
|
}
|
||||||
|
}
|
||||||
175
mtucijobsweb/fsd/widgets/Search/SearchWidget.tsx
Normal file
175
mtucijobsweb/fsd/widgets/Search/SearchWidget.tsx
Normal file
@@ -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<number | undefined>();
|
||||||
|
const [qualification, setQualification] = useState<boolean | undefined>();
|
||||||
|
const [time, setTime] = useState<string[]>([]);
|
||||||
|
const [salary, setSalary] = useState<number | undefined>();
|
||||||
|
const [hardskills, setHardskills] = useState<string[]>([]);
|
||||||
|
const [searchResults, setSearchResults] = useState<JobData[]>([]);
|
||||||
|
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<JobData[]>(
|
||||||
|
['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 <Spin size='large' className={style.spin} />;
|
||||||
|
if (error)
|
||||||
|
return <div className={style.error}>Ошибка при поиске вакансий</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.container}>
|
||||||
|
<div className={style.form}>
|
||||||
|
<Input
|
||||||
|
placeholder='Поиск по названию вакансии или компании...'
|
||||||
|
value={query}
|
||||||
|
onChange={e => setQuery(e.target.value)}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
style={{ marginBottom: 20 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder='Выберите Курс'
|
||||||
|
style={{ width: '100%', marginBottom: 20 }}
|
||||||
|
onChange={value => setYear(value)}
|
||||||
|
value={year}
|
||||||
|
>
|
||||||
|
<Option value={1}>1</Option>
|
||||||
|
<Option value={2}>2</Option>
|
||||||
|
<Option value={3}>3</Option>
|
||||||
|
<Option value={4}>4</Option>
|
||||||
|
{/* Добавьте другие годы по необходимости */}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
checked={qualification}
|
||||||
|
onChange={e => setQualification(e.target.checked)}
|
||||||
|
style={{ marginBottom: 20, color: themeParams.textColor }}
|
||||||
|
>
|
||||||
|
Требуется квалификация
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
mode='multiple'
|
||||||
|
placeholder='Выберите занятость'
|
||||||
|
style={{ width: '100%', marginBottom: 20 }}
|
||||||
|
onChange={value => setTime(value)}
|
||||||
|
value={time}
|
||||||
|
>
|
||||||
|
<Option value='20'>20</Option>
|
||||||
|
<Option value='30'>30</Option>
|
||||||
|
<Option value='40'>40</Option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<InputNumber
|
||||||
|
placeholder='Минимальная зарплата'
|
||||||
|
value={salary}
|
||||||
|
onChange={value => setSalary(value !== null ? value : undefined)}
|
||||||
|
style={{ width: '100%', marginBottom: 20 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
mode='multiple'
|
||||||
|
placeholder='Выберите hard skills'
|
||||||
|
style={{ width: '100%', marginBottom: 20 }}
|
||||||
|
onChange={value => setHardskills(value)}
|
||||||
|
value={hardskills}
|
||||||
|
options={skillsOptions.map(skill => ({ value: skill, label: skill }))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type='primary' onClick={handleSearch} style={{ width: '100%' }}>
|
||||||
|
Поиск
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
{isLoading && <Spin size='large' />}
|
||||||
|
{searchResults.length > 0 ? (
|
||||||
|
<div className={style.cardWrapper}>
|
||||||
|
{searchResults.map((item: JobData) => (
|
||||||
|
<Card
|
||||||
|
key={item.Email} // Предположим, что Email уникален для вакансий
|
||||||
|
title={item.Job_name}
|
||||||
|
bordered={false}
|
||||||
|
style={{ marginBottom: 20 }}
|
||||||
|
className={style.card}
|
||||||
|
>
|
||||||
|
<p>Год: {item.Year}</p>
|
||||||
|
<p>Квалификация: {item.Qualification ? 'Да' : 'Нет'}</p>
|
||||||
|
<p>Зарплата: {item.Salary}</p>
|
||||||
|
<p>Soft Skills: {item.Soft_skills}</p>
|
||||||
|
<p>Обязанности: {item.Responsibilities}</p>
|
||||||
|
<p>
|
||||||
|
Email: <a href={`mailto:${item.Email}`}>{item.Email}</a>
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
!isLoading && (
|
||||||
|
<div style={{ color: themeParams.textColor }}>
|
||||||
|
Вакансии не найдены
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchWidget;
|
||||||
10
mtucijobsweb/next.config.js
Normal file
10
mtucijobsweb/next.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: false,
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_APP_BASE_URL: process.env.NEXT_PUBLIC_APP_BASE_URL,
|
||||||
|
NEXT_PUBLIC_BOT_URL: process.env.NEXT_PUBLIC_BOT_URL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
6178
mtucijobsweb/package-lock.json
generated
Normal file
6178
mtucijobsweb/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
mtucijobsweb/package.json
Normal file
35
mtucijobsweb/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "mtucijobsweb",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tma.js/launch-params": "^1.0.1",
|
||||||
|
"@tma.js/sdk": "^2.5.1",
|
||||||
|
"@tma.js/sdk-react": "^2.2.5",
|
||||||
|
"antd": "^5.18.2",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"next": "13.5.6",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-query": "^3.39.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "^13.5.6",
|
||||||
|
"postcss": "^8",
|
||||||
|
"sass": "^1.77.6",
|
||||||
|
"tailwindcss": "^3",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
mtucijobsweb/postcss.config.js
Normal file
6
mtucijobsweb/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
mtucijobsweb/public/next.svg
Normal file
1
mtucijobsweb/public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
mtucijobsweb/public/vercel.svg
Normal file
1
mtucijobsweb/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||||
|
After Width: | Height: | Size: 629 B |
15
mtucijobsweb/run.sh
Normal file
15
mtucijobsweb/run.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker stop mtuci-jobs-web || echo "1"
|
||||||
|
docker rm mtuci-jobs-web || echo "2"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--restart always \
|
||||||
|
-h mtuci-jobs-web \
|
||||||
|
-p 127.0.0.1:3000:3000 \
|
||||||
|
-e APP_BASE_URL="" \
|
||||||
|
--name mtuci-jobs-web \
|
||||||
|
--log-opt mode=non-blocking \
|
||||||
|
--log-opt max-size=10m \
|
||||||
|
--log-opt max-file=3 \
|
||||||
|
mtuci-jobs-web-image:latest
|
||||||
20
mtucijobsweb/tailwind.config.ts
Normal file
20
mtucijobsweb/tailwind.config.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
|
'gradient-conic':
|
||||||
|
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
export default config
|
||||||
27
mtucijobsweb/tsconfig.json
Normal file
27
mtucijobsweb/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
60
mtucijobsweb/types/types.ts
Normal file
60
mtucijobsweb/types/types.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
interface Time {
|
||||||
|
hour: string;
|
||||||
|
}
|
||||||
|
export interface FormValues {
|
||||||
|
Name: string;
|
||||||
|
Type: string;
|
||||||
|
Group: string;
|
||||||
|
Faculties:string;
|
||||||
|
Phone_number:string;
|
||||||
|
Time: Time[];
|
||||||
|
skills: string[];
|
||||||
|
Soft_skills: string;
|
||||||
|
Email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Student extends Omit<FormValues, 'skills'> {
|
||||||
|
StudentID: number;
|
||||||
|
Link: string;
|
||||||
|
Hardskills: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type HardSkill = {
|
||||||
|
Title: string;
|
||||||
|
};
|
||||||
|
export interface Request {
|
||||||
|
StudentID: number;
|
||||||
|
Name: string;
|
||||||
|
Type: string;
|
||||||
|
Group: string;
|
||||||
|
Faculties: string;
|
||||||
|
Phone_number: string;
|
||||||
|
Time: Time[]; // массив строк для времени
|
||||||
|
Soft_skills: string;
|
||||||
|
Link: string;
|
||||||
|
Email: string;
|
||||||
|
Hardskills: string[]; // массив навыков
|
||||||
|
}
|
||||||
|
export interface Bot {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobsSearch {
|
||||||
|
year?: string;
|
||||||
|
qualification?: boolean;
|
||||||
|
time?: string[];
|
||||||
|
salary?: number;
|
||||||
|
hardskills?: string[];
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
export interface JobData {
|
||||||
|
JobID: number;
|
||||||
|
Company_name: string;
|
||||||
|
Job_name: string;
|
||||||
|
Year: string;
|
||||||
|
Qualification: boolean;
|
||||||
|
Soft_skills: string;
|
||||||
|
Salary: number;
|
||||||
|
Email: string;
|
||||||
|
Responsibilities: string;
|
||||||
|
}
|
||||||
4
mtucijobsweb2/.eslintrc.json
Normal file
4
mtucijobsweb2/.eslintrc.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
35
mtucijobsweb2/.gitignore
vendored
Normal file
35
mtucijobsweb2/.gitignore
vendored
Normal file
@@ -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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user