diff --git a/db.sql b/db.sql index e7a205a..c3f5491 100644 Binary files a/db.sql and b/db.sql differ diff --git a/functions/admin/get_log.py b/functions/admin/get_log.py new file mode 100644 index 0000000..cf6e57c --- /dev/null +++ b/functions/admin/get_log.py @@ -0,0 +1,21 @@ +import os +from typing import List, Dict, Any + + +def get_log(log_id: int) -> List[Dict[str, Any]]: + log_dir = os.path.join("static/logs", str(log_id)) + return_dir = [] + for dir_path, _, filenames in os.walk(log_dir): + if dir_path != log_dir: + audio_file = os.path.join(dir_path, "audio.ogg") + if not os.path.exists(audio_file): + audio_file = os.path.join(dir_path, "audio.wav") + text_file = os.path.join(dir_path, "yandex-text.txt") + if not os.path.exists(text_file): + text_file = os.path.join(dir_path, "google-text.txt") + try: + return_dir.append({"id": log_id, "audio_file": f"/{audio_file}", + "text": open(text_file).read().split("\n")}) + except UnicodeDecodeError: + pass + return return_dir diff --git a/functions/admin/get_logs.py b/functions/admin/get_logs.py new file mode 100644 index 0000000..9881637 --- /dev/null +++ b/functions/admin/get_logs.py @@ -0,0 +1,8 @@ +import os +from typing import List, Tuple + + +def get_logs() -> List[Tuple[int, str]]: + return [(int(os.path.basename(dir_path)), dir_path.split("/")[2].strip()) + for dir_path, _, filenames in os.walk("static/logs") + if dir_path != "static/logs" and len(dir_path.split("/")) == 3] diff --git a/functions/admin/is_logged_in.py b/functions/admin/is_logged_in.py new file mode 100644 index 0000000..ae91cab --- /dev/null +++ b/functions/admin/is_logged_in.py @@ -0,0 +1,16 @@ +import jwt +from fastapi import Cookie, Depends +from functions.admin.models import token, database + + +def is_logged_in(access_token: str = Cookie(None), db=Depends(database.get_db)): + if not access_token: + return False + response = db.query(token.Token).filter(token.Token.access_token == access_token).first() + if not response or not response.is_active: + return False + try: + jwt.decode(access_token, "secret", algorithms=["HS256"]) + except jwt.exceptions.ExpiredSignatureError: + return False + return True diff --git a/functions/admin/models/database.py b/functions/admin/models/database.py new file mode 100644 index 0000000..e8a19b0 --- /dev/null +++ b/functions/admin/models/database.py @@ -0,0 +1,18 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./db.sql" + +engine = create_engine(SQLALCHEMY_DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() +Base.metadata.create_all(bind=engine) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/functions/admin/models/token.py b/functions/admin/models/token.py new file mode 100644 index 0000000..230350f --- /dev/null +++ b/functions/admin/models/token.py @@ -0,0 +1,14 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from functions.admin.models.database import Base + +class Token(Base): + __tablename__ = "tokens" + + id = Column(Integer, primary_key=True, index=True) + access_token = Column(String(255), unique=True) + refresh_token = Column(String(255), unique=True) + is_active = Column(Boolean(), default=True) + + user_id = Column(Integer, ForeignKey("users.id")) + user = relationship("User", back_populates="tokens") diff --git a/functions/admin/models/user.py b/functions/admin/models/user.py new file mode 100644 index 0000000..5879ced --- /dev/null +++ b/functions/admin/models/user.py @@ -0,0 +1,19 @@ +import hashlib + +from sqlalchemy import Boolean, Column, Integer, String +from sqlalchemy.orm import relationship +from functions.admin.models.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True) + email = Column(String(255), unique=True) + password_hash = Column(String(255)) + is_active = Column(Boolean(), default=True) + + tokens = relationship("Token", back_populates="user") + + def check_password(self, password: str): + return self.password_hash == hashlib.sha384(password.encode()).hexdigest() diff --git a/functions/admin/templates/log.py b/functions/admin/templates/log.py new file mode 100644 index 0000000..0c83f84 --- /dev/null +++ b/functions/admin/templates/log.py @@ -0,0 +1,10 @@ +from fastapi import Request, Depends, responses, HTTPException +from functions.admin import get_log, is_logged_in + +async def log(templates, request: Request, log_id: int, logged_in: bool = Depends(is_logged_in.is_logged_in)): + if logged_in: + if not get_log.get_log(log_id): + raise HTTPException(status_code=400) + return templates.TemplateResponse("log.html", {"request": request, "log": get_log.get_log(log_id)}) + else: + return responses.RedirectResponse(url=f"/refresh?source=/logs/{log_id}", status_code=303) diff --git a/functions/admin/templates/logins.py b/functions/admin/templates/logins.py new file mode 100644 index 0000000..40aa07f --- /dev/null +++ b/functions/admin/templates/logins.py @@ -0,0 +1,25 @@ +from fastapi import Form, Depends, responses, HTTPException +from functions.admin.models import database, user, token +import jwt +from datetime import datetime, timedelta + + +async def logins(username: str = Form(...), password: str = Form(...), db=Depends(database.get_db)): + # username = "Dmitrium12" + response = db.query(user.User).filter(user.User.username == username).first() + if not response or not response.check_password(password): + raise HTTPException(status_code=400, detail="Неправильное имя пользователя или пароль") + access_token_expires = datetime.utcnow() + timedelta(minutes=15) + access_token_payload = {"sub": response.username, "exp": access_token_expires} + access_token = jwt.encode(access_token_payload, "secret", algorithm="HS256") + refresh_token_expires = datetime.utcnow() + timedelta(days=7) + refresh_token_payload = {"sub": response.username, "exp": refresh_token_expires} + refresh_token = jwt.encode(refresh_token_payload, "secret", algorithm="HS256") + db_token = token.Token(access_token=access_token, refresh_token=refresh_token, user=response) + db.add(db_token) + db.commit() + response = responses.RedirectResponse(url="/logs", status_code=303) + response.set_cookie(key="access_token", value=access_token, expires=int(access_token_expires.timestamp())) + response.set_cookie(key="refresh_token", value=refresh_token, + expires=int(refresh_token_expires.timestamp())) + return response diff --git a/functions/admin/templates/logs.py b/functions/admin/templates/logs.py new file mode 100644 index 0000000..f913a79 --- /dev/null +++ b/functions/admin/templates/logs.py @@ -0,0 +1,8 @@ +from fastapi import Request, Depends, responses +from functions.admin import get_logs, is_logged_in + +async def logs(templates, request: Request, logged_in: bool = Depends(is_logged_in.is_logged_in)): + if logged_in: + return templates.TemplateResponse("logs.html", {"request": request, "logs": get_logs.get_logs()}) + else: + return responses.RedirectResponse(url="/refresh?source=/logs", status_code=303) diff --git a/functions/admin/templates/refresh.py b/functions/admin/templates/refresh.py new file mode 100644 index 0000000..1a9aded --- /dev/null +++ b/functions/admin/templates/refresh.py @@ -0,0 +1,31 @@ +from fastapi import Request, Depends, Cookie, responses +from functions.admin.models import database, user, token +import jwt +from datetime import datetime, timedelta + +async def refresh_access_token(req: Request, refresh_token: str = Cookie(None), db=Depends(database.get_db)): + request_args = dict(req.query_params) + try: + refresh_token_payload = jwt.decode(refresh_token, "secret", algorithms=["HS256"]) + except jwt.exceptions.DecodeError: + return responses.RedirectResponse(url="/login", status_code=303) + response = db.query(user.User).filter(user.User.username == refresh_token_payload["sub"]).first() + if not response: + return responses.RedirectResponse(url="/login", status_code=303) + access_token_expires = datetime.utcnow() + timedelta(minutes=15) + access_token_payload = {"sub": response.username, "exp": access_token_expires} + access_token = jwt.encode(access_token_payload, "secret", algorithm="HS256") + db.query(token.Token).filter(token.Token.refresh_token == refresh_token).update({ + token.Token.access_token: access_token, + }) + db.commit() + if request_args: + response = responses.RedirectResponse(url=request_args["source"], status_code=303) + else: + response = responses.RedirectResponse(url="/", status_code=303) + response.set_cookie( + key="access_token", + value=access_token, + expires=int(access_token_expires.timestamp()) + ) + return response diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f978e34 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,127 @@ +aiofiles==22.1.0 +aiogram==2.23.1 +aiohttp==3.8.3 +aiolimiter==1.0.0 +aiosignal==1.3.1 +anyio==3.6.2 +APScheduler==3.10.1 +async-generator==1.10 +async-timeout==4.0.2 +attrs==22.1.0 +audiosegment==0.23.0 +Babel==2.9.1 +bcrypt==4.0.1 +beautifulsoup4==4.11.1 +BibaAndBoba==1.2.2 +boto3==1.26.13 +botocore==1.29.13 +cachetools==5.3.0 +certifi==2022.9.24 +cffi==1.15.1 +charset-normalizer==2.1.1 +click==8.1.3 +contourpy==1.0.6 +cryptography==38.0.3 +cycler==0.11.0 +DAWG-Python==0.7.2 +deep-translator==1.9.2 +dill==0.3.6 +discord==2.2.2 +discord.py==2.2.2 +dnspython==2.3.0 +docopt==0.6.2 +email-validator==1.3.1 +exceptiongroup==1.1.1 +fastapi==0.95.1 +fastapi-users==10.4.2 +Flask==2.2.3 +fonttools==4.38.0 +frozenlist==1.3.3 +geographiclib==2.0 +geopy==2.3.0 +greenlet==2.0.2 +grpcio==1.50.0 +grpcio-tools==1.48.2 +h11==0.14.0 +h2==4.1.0 +hpack==4.0.0 +httpcore==0.16.1 +httpx==0.23.3 +hyperframe==6.0.1 +idna==3.4 +itsdangerous==2.1.2 +javascript==1!1.0.1 +Jinja2==3.1.2 +jmespath==1.0.1 +joblib==1.2.0 +kiwisolver==1.4.4 +loguru==0.6.0 +lxml==4.9.2 +magic-filter==1.0.9 +makefun==1.15.1 +MarkupSafe==2.1.2 +matplotlib==3.6.2 +multidict==6.0.3 +multiprocess==0.70.14 +mutagen==1.46.0 +nltk==3.8 +numpy==1.23.5 +openai==0.27.4 +outcome==1.2.0 +packaging==22.0 +pandas==1.4.3 +passlib==1.7.4 +pathos==0.3.0 +Pillow==9.4.0 +pox==0.3.2 +ppft==1.7.6.6 +protobuf==3.19.6 +pycparser==2.21 +pydantic==1.10.2 +pydub==0.25.1 +PyExecJS==1.5.1 +PyJWT==2.6.0 +pymorphy2==0.9.1 +pymorphy2-dicts-ru==2.4.417127.4579844 +pyparsing==3.0.9 +PySocks==1.7.1 +pyTelegramBotAPI==4.7.1 +python-dateutil==2.8.2 +python-dotenv==1.0.0 +python-multipart==0.0.6 +python-telegram-bot==20.1 +pytz==2022.6 +pytz-deprecation-shim==0.1.0.post0 +regex==2022.10.31 +requests==2.28.1 +rfc3986==1.5.0 +rnnoise-wrapper @ git+https://github.com/Desklop/RNNoise_Wrapper@10647eba5c1dc678dc3fd443d111400792fefef6 +s3transfer==0.6.0 +scipy==1.10.0 +selenium==4.8.3 +six==1.16.0 +sniffio==1.3.0 +sortedcontainers==2.4.0 +soupsieve==2.3.2.post1 +speechkit==2.1.1 +SpeechRecognition==3.8.1 +SQLAlchemy==2.0.10 +starlette==0.26.1 +tornado==6.2 +tqdm==4.64.1 +translators==5.5.6 +trio==0.22.0 +trio-websocket==0.10.2 +typing_extensions==4.4.0 +tzdata==2022.7 +tzlocal==4.3 +urllib3==1.26.12 +uvicorn==0.21.1 +vk-api==11.9.9 +webdriver-manager==3.8.6 +webrtcvad==2.0.10 +Werkzeug==2.2.3 +wsproto==1.2.0 +yandex-s3==0.1.1 +yarl==1.8.2 +yaweather==1.2.2 diff --git a/static/css/index.css b/static/css/index.css index 59d5d55..28217fe 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -1,52 +1,139 @@ +/* Global styles */ body { - background-color: #333; - color: #fff; font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5; margin: 0; padding: 0; + background-color: #333; } -h1 { - text-align: center; - margin-top: 50px; - margin-bottom: 30px; +a { + color: #333; } -form { +ul { + list-style: none; + margin: 0; + padding: 0; +} + +/* Header styles */ +header { + background-color: #333; +} + +nav { display: flex; - flex-direction: column; + justify-content: space-between; align-items: center; + margin: 1rem; } -label { - margin-bottom: 10px; +nav ul { + display: flex; + justify-content: flex-end; + align-items: center; + height: auto; + width: auto; + padding-left: 0; } -input[type="password"] { - padding: 10px; - border-radius: 5px; - border: none; - background-color: #555; - color: #fff; - width: 100%; - max-width: 300px; /* добавлено, чтобы форма не была слишком широкой */ - box-sizing: border-box; /* добавлено, чтобы input не выходил за границы родительского элемента */ - margin-bottom: 20px; /* добавлено, чтобы был отступ между input и кнопкой */ +nav li { + margin-right: 20px; + position: relative } -button[type="submit"] { +nav a { + text-decoration: none; + color: #333; + text-transform: uppercase +} + +.auth-buttons a { + -webkit-appearance: button; + -moz-appearance: button; + text-decoration: none; padding: 10px 20px; border-radius: 25px; /* более округлая форма */ border: none; background-color: #4CAF50; color: #fff; cursor: pointer; - box-shadow: 0 6px 0 #3e8e41; /* тень при наведении курсора */ + box-shadow: 0 5px 0 #3e8e41; /* тень при наведении курсора */ transition: box-shadow 0.2s ease-in-out; } -button[type="submit"]:hover { +.auth-buttons a:hover { box-shadow: 0 3px 0 #3e8e41; +} + + +/* Bot section styles */ +.bot-section { + margin-top: 2rem; + padding: 2rem; + background-color: #333; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + text-align: center +} + + +.bot-section h1 { + font-size: 2.5rem; +} + + +.bot-image img { + width: 300px; + height: auto; + margin-bottom: 2rem +} + + +.bot-features h2 { + font-size: 1.5rem; + margin-bottom: 1rem +} + + +.bot-features ul { + margin-bottom: 2rem +} + + +.bot-features li { + margin-bottom: .5rem +} + + +.bot-usage h2 { + font-size: 1.5rem; + margin-bottom: 1rem +} + + +.bot-usage ol { + margin-bottom: 2rem; + text-align: left +} + +.bot-usage li { + margin-bottom: .5rem +} + +.bot-feedback h2 { + font-size: 1.5rem; + margin-bottom: 1rem +} + +/* Footer styles */ +footer { + background-color: #1e1e1e; + color: #fff; + text-align: center; + padding: 1rem; } \ No newline at end of file diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..6bbe4d6 --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,53 @@ +body { + background-color: #333; + color: #fff; + font-family: Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + margin: 0; + padding: 0; +} + +h1 { + text-align: center; + margin-top: 50px; + margin-bottom: 30px; +} + +form { + display: flex; + flex-direction: column; + align-items: center; +} + +label { + margin-bottom: 10px; +} + +input { + padding: 10px; + border-radius: 5px; + border: none; + background-color: #555; + color: #fff; + width: 100%; + max-width: 300px; /* добавлено, чтобы форма не была слишком широкой */ + box-sizing: border-box; /* добавлено, чтобы input не выходил за границы родительского элемента */ + margin-bottom: 20px; /* добавлено, чтобы был отступ между input и кнопкой */ +} + +button[type="submit"] { + padding: 10px 20px; + border-radius: 25px; /* более округлая форма */ + border: none; + background-color: #4CAF50; + color: #fff; + cursor: pointer; + box-shadow: 0 6px 0 #3e8e41; /* тень при наведении курсора */ + transition: box-shadow 0.2s ease-in-out; + width: 100px; +} + +button[type="submit"]:hover { + box-shadow: 0 3px 0 #3e8e41; +} \ No newline at end of file diff --git a/static/templates/index.html b/static/templates/index.html index fb69257..9a56e87 100644 --- a/static/templates/index.html +++ b/static/templates/index.html @@ -1,17 +1,61 @@
-Разработанный нами Telegram-бот может переводить голосовые сообщения и общаться с ChatGPT, + сохраняя историю запросов. Этот бот создан для того, чтобы облегчить вашу жизнь, + предоставляя вам безупречный опыт перевода голосовых сообщений и общения с ChatGPT без необходимости + переключаться между различными приложениями. Бот интегрирован в Telegram, что означает, + что вы можете использовать его на любом устройстве, поддерживающем этот мессенджер.
+