copied the code from the working repo

This commit is contained in:
2024-11-30 16:00:48 +03:00
parent f22b92869b
commit 15ac0cb9b8
148 changed files with 23342 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.env

33
Jenkinsfile vendored Normal file
View 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
View 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
View 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

View File

@@ -0,0 +1,5 @@
database_hostname =
database_port =
database_password =
database_name =
database_username =

3
mtucijobsbackend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
__pycache__
venv/
.env

View 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/"]

View File

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

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

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

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

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

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

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

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

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

View 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

View 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

View 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

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

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

View 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

View File

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

View 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

View File

@@ -0,0 +1,13 @@
**/node_modules
*.md
*.env
Dockerfile
docker-compose.yml
**/.npmignore
**/.dockerignore
**/*.md
**/*.log
**/.vscode
**/.git
**/.eslintrc.json
*.sh

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

@@ -0,0 +1,4 @@
.DS_store
**/.DS_Store
node_modules
.env

12
mtucijobsbot/Dockerfile Normal file
View 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
View 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
View 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
View 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;

Binary file not shown.

90
mtucijobsbot/dist/commands/start.js vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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 = {}));

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

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

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

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

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

View 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 = {};

View 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

File diff suppressed because it is too large Load Diff

37
mtucijobsbot/package.json Normal file
View 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
View 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

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

Binary file not shown.

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

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

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

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

View 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(
'Произошла ошибка при обновлении языка. Пожалуйста, попробуйте снова позже.'
);
}
}
}

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

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

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

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

View 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
View 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. */
}
}

View File

@@ -0,0 +1,12 @@
**/node_modules
*.md
Dockerfile
docker-compose.yml
**/.npmignore
**/.dockerignore
**/*.md
**/*.log
**/.vscode
**/.git
**/.eslintrc.json
*.sh

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['next', 'next/core-web-vitals'],
};

35
mtucijobsweb/.gitignore vendored Normal file
View 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
View 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
View 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
View 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;
}
};

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

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

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

@@ -0,0 +1,7 @@
#!/bin/bash
docker build . \
--build-arg APP_BASE_URL="" \
--no-cache \
--rm \
--pull \
-t mtuci-jobs-web-image:latest

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

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

View 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',
];

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

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

View File

@@ -0,0 +1,13 @@
'use client'
import SearchWidget from '../widgets/Search/SearchWidget';
const SearchPage: React.FC = () => {
return (
<>
<SearchWidget />
</>
);
};
export default SearchPage;

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

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

View 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); /* Тень при наведении */
}
}

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

View 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

File diff suppressed because it is too large Load Diff

35
mtucijobsweb/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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

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

View 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

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

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

View File

@@ -0,0 +1,4 @@
{
"root": true,
"extends": "next/core-web-vitals"
}

35
mtucijobsweb2/.gitignore vendored Normal file
View 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