commit 4dee2bb54ca0d975954a6977ef14a7debab89a7a Author: drholy Date: Sun Sep 14 17:15:48 2025 +0000 init diff --git a/.env b/.env new file mode 100644 index 0000000..5c9d32b --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +TELEGRAM_TOKEN=8039533737:AAF0DKayjO21LRNCwdh8UffltXLg1HameGQ +OPENROUTER_API_KEY=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 +DATABASE_URL=postgresql+asyncpg://drholy:SportForever1999@db:5432/chatdb +WEBAPP_URL=https://llm.byte-mate.xyz/webapp +DOMAIN=llm.byte-mate.xyz +EMAIL=your@email.com +POSTGRES_USER=drholy +POSTGRES_PASSWORD=SportForever1999 +POSTGRES_DB=chatdb \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2239bd9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11 + +WORKDIR /code + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/app/__pycache__/api.cpython-311.pyc b/app/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000..b649411 Binary files /dev/null and b/app/__pycache__/api.cpython-311.pyc differ diff --git a/app/__pycache__/crud.cpython-311.pyc b/app/__pycache__/crud.cpython-311.pyc new file mode 100644 index 0000000..a2333a5 Binary files /dev/null and b/app/__pycache__/crud.cpython-311.pyc differ diff --git a/app/__pycache__/database.cpython-311.pyc b/app/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000..0962272 Binary files /dev/null and b/app/__pycache__/database.cpython-311.pyc differ diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..fdc0b11 Binary files /dev/null and b/app/__pycache__/main.cpython-311.pyc differ diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..5260b30 Binary files /dev/null and b/app/__pycache__/models.cpython-311.pyc differ diff --git a/app/__pycache__/openrouter_client.cpython-311.pyc b/app/__pycache__/openrouter_client.cpython-311.pyc new file mode 100644 index 0000000..4d8fe3e Binary files /dev/null and b/app/__pycache__/openrouter_client.cpython-311.pyc differ diff --git a/app/__pycache__/schemas.cpython-311.pyc b/app/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000..7fc866c Binary files /dev/null and b/app/__pycache__/schemas.cpython-311.pyc differ diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..b67eaed --- /dev/null +++ b/app/api.py @@ -0,0 +1,101 @@ +# from fastapi import APIRouter, Depends +# from sqlalchemy.ext.asyncio import AsyncSession +# from .database import async_session +# from . import crud, openrouter_client + +# router = APIRouter() + +# async def get_db(): +# async with async_session() as db: +# yield db + +# @router.get("/history/{telegram_id}") +# async def get_history(telegram_id: str, db: AsyncSession = Depends(get_db)): +# user = await crud.get_or_create_user(db, telegram_id) +# history = await crud.get_conversation(db, user, limit=20) +# return [{"role": m.role, "content": m.content} for m in history] + +# @router.post("/chat/{telegram_id}") +# async def chat(telegram_id: str, payload: dict, db: AsyncSession = Depends(get_db)): +# user = await crud.get_or_create_user(db, telegram_id) +# user_msg = payload.get("message", "") +# if not user_msg: +# return {"error": "Empty message"} + +# await crud.add_message(db, user, "user", user_msg) + +# history = await crud.get_conversation(db, user, limit=10) +# messages_payload = [{"role": m.role, "content": m.content} for m in history] + +# response = await openrouter_client.generate_response(messages_payload) +# await crud.add_message(db, user, "assistant", response) + +# return {"response": response} + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy.orm import selectinload +from app import models, schemas +from .database import async_session + +router = APIRouter() + +# --- Dependency --- +async def get_db() -> AsyncSession: + async with async_session() as db: + yield db + + +# 📌 Получить профиль пользователя (берём первого для примера) +@router.get("/profile", response_model=schemas.UserOut) +async def get_profile(db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(models.User).options( + selectinload(models.User.chats).selectinload(models.Chat.messages) + ) + ) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +# 📌 Получить список чатов +@router.get("/chats", response_model=list[schemas.ChatOut]) +async def get_chats(db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(models.Chat).options(selectinload(models.Chat.messages)) + ) + return result.scalars().all() + + +# 📌 Сообщения конкретного чата +@router.get("/chats/{chat_id}/messages", response_model=list[schemas.MessageOut]) +async def get_chat_messages(chat_id: int, db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(models.Chat) + .options(selectinload(models.Chat.messages)) + .filter_by(id=chat_id) + ) + chat = result.scalars().first() + if not chat: + raise HTTPException(status_code=404, detail="Chat not found") + return chat.messages + + +# 📌 Добавить новое сообщение +@router.post("/chats/{chat_id}/messages", response_model=schemas.MessageOut) +async def add_message(chat_id: int, msg: schemas.MessageBase, db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(models.Chat).filter_by(id=chat_id) + ) + chat = result.scalars().first() + if not chat: + raise HTTPException(status_code=404, detail="Chat not found") + + new_msg = models.Message(chat_id=chat_id, role=msg.role, content=msg.content) + db.add(new_msg) + await db.commit() + await db.refresh(new_msg) + return new_msg \ No newline at end of file diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 0000000..416e0fc --- /dev/null +++ b/app/crud.py @@ -0,0 +1,29 @@ +from sqlalchemy.future import select +from sqlalchemy.ext.asyncio import AsyncSession +from . import models + +async def get_or_create_user(db: AsyncSession, telegram_id: str): + result = await db.execute(select(models.User).where(models.User.telegram_id == telegram_id)) + user = result.scalar_one_or_none() + if not user: + user = models.User(telegram_id=telegram_id) + db.add(user) + await db.commit() + await db.refresh(user) + return user + +async def add_message(db: AsyncSession, user, role, content): + msg = models.Message(user_id=user.id, role=role, content=content) + db.add(msg) + await db.commit() + await db.refresh(msg) + return msg + +async def get_conversation(db: AsyncSession, user, limit=10): + result = await db.execute( + select(models.Message) + .where(models.Message.user_id == user.id) + .order_by(models.Message.timestamp.desc()) + .limit(limit) + ) + return list(reversed(result.scalars().all())) \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..fb9ed0d --- /dev/null +++ b/app/database.py @@ -0,0 +1,9 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker, declarative_base +import os + +DATABASE_URL = os.getenv("DATABASE_URL") + +engine = create_async_engine(DATABASE_URL, echo=True, future=True) +async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) +Base = declarative_base() \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f997448 --- /dev/null +++ b/app/main.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from . import database, models, api + +from app.api import router as api_router + +app = FastAPI() + +app.include_router(api_router, prefix="/api") + +@app.on_event("startup") +async def startup(): + async with database.engine.begin() as conn: + await conn.run_sync(models.Base.metadata.create_all) + +@app.get("/{full_path:path}") +async def serve_spa(full_path: str): + index_path = os.path.join(dist_path, "index.html") + return FileResponse(index_path) \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..9b11f10 --- /dev/null +++ b/app/models.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, func +from sqlalchemy.orm import relationship +from app.database import Base + +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True, index=True) + telegram_id = Column(String, unique=True, index=True) + username = Column(String, nullable=True) + + chats = relationship("Chat", back_populates="owner") + + +class Chat(Base): + __tablename__ = "chats" + id = Column(Integer, primary_key=True, index=True) + title = Column(String, default="Новый чат") + + owner_id = Column(Integer, ForeignKey("users.id")) + owner = relationship("User", back_populates="chats") + + messages = relationship("Message", back_populates="chat", cascade="all, delete-orphan") + + +class Message(Base): + __tablename__ = "messages" + id = Column(Integer, primary_key=True, index=True) + role = Column(String) # 'user' | 'assistant' | 'system' + content = Column(Text) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + chat_id = Column(Integer, ForeignKey("chats.id")) + chat = relationship("Chat", back_populates="messages") \ No newline at end of file diff --git a/app/openrouter_client.py b/app/openrouter_client.py new file mode 100644 index 0000000..6c6bbd1 --- /dev/null +++ b/app/openrouter_client.py @@ -0,0 +1,33 @@ +import os +from openai import OpenAI + +OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY") +SITE_URL = os.getenv("WEBAPP_URL", "https://llm.byte-mate.xyz") +SITE_NAME = "Telegram LLM Chat" + +# инициализируем клиента +client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=OPENROUTER_API_KEY, +) + +async def generate_response(messages): + """ + messages: список сообщений [{'role': 'user'|'assistant'|'system', 'content': str}] + """ + try: + completion = client.chat.completions.create( + # дополнительные заголовки для OpenRouter (не обязательно, но полезно) + extra_headers={ + "HTTP-Referer": SITE_URL, + "X-Title": SITE_NAME, + }, + model="qwen/qwen3-235b-a22b:free", # ✅ используй модель, которая точно доступна твоему API‑ключу + messages=messages, + ) + return completion.choices[0].message.content + + except Exception as e: + # Логи и возврат ошибки + print(f"❌ Ошибка вызова OpenRouter API: {e}") + return "Извините, LLM сейчас недоступен. Попробуйте позже." \ No newline at end of file diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..0e6d7a2 --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime + +class MessageBase(BaseModel): + role: str + content: str + +class MessageOut(MessageBase): + id: int + created_at: datetime + + class Config: + orm_mode = True + + +class ChatBase(BaseModel): + title: str + +class ChatOut(ChatBase): + id: int + messages: List[MessageOut] = [] + + class Config: + orm_mode = True + + +class UserOut(BaseModel): + id: int + telegram_id: str + username: Optional[str] + chats: List[ChatOut] = [] + + class Config: + orm_mode = True \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..9b71944 --- /dev/null +++ b/bot.py @@ -0,0 +1,32 @@ +import os +import asyncio +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo +from telegram.ext import Application, CommandHandler, ContextTypes + +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +WEBAPP_URL = os.getenv("WEBAPP_URL", "https://yourdomain.com/webapp") # из .env или дефолт + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка команды /start""" + keyboard = [ + [InlineKeyboardButton("🚀 Открыть чат", web_app=WebAppInfo(url=WEBAPP_URL))] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text( + "Привет! Нажми кнопку ниже, чтобы открыть WebApp 👇", + reply_markup=reply_markup + ) + +def main(): + # Создаём приложение + app = Application.builder().token(TELEGRAM_TOKEN).build() + + # Добавляем обработчик команды /start + app.add_handler(CommandHandler("start", start)) + + # Запускаем (асинхронно работает внутри run_polling) + print("🚀 Бот запущен. Нажми /start в чате с ним.") + app.run_polling() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..559b414 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +services: + web: + build: . + container_name: telegram-llm-chat + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + volumes: + - .:/code + expose: + - "8000" + depends_on: + - db + env_file: + - .env + + bot: + build: . + container_name: telegram-bot + command: python bot.py + volumes: + - .:/code + depends_on: + - web # бот будет запускаться после backend API + env_file: + - .env + restart: always # если упадёт - перезапустится + + db: + image: postgres:15 + container_name: chatdb + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - db-data:/var/lib/postgresql/data + + nginx: + build: + context: . + dockerfile: frontend/Dockerfile + container_name: nginx_proxy + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - certbot-etc:/etc/letsencrypt + - certbot-var:/var/lib/letsencrypt + - ./nginx/certbot:/var/www/certbot + depends_on: + - web + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - certbot-etc:/etc/letsencrypt + - certbot-var:/var/lib/letsencrypt + - ./nginx/certbot:/var/www/certbot + depends_on: + - nginx + # Закоментируй entrypoint для первого выпуска сертификата!!! + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h; done'" + +volumes: + db-data: + certbot-etc: + certbot-var: + frontend-html: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..c9ab0a7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,29 @@ +# ---------- STAGE 1: Build React App ---------- +FROM node:22-alpine AS build +WORKDIR /app + +# копируем package.json и package-lock.json из frontend/ +COPY frontend/package*.json ./ +RUN npm install + +# копируем весь src проекта фронта +COPY frontend/ ./ +RUN npm run build + +# ---------- STAGE 2: Nginx ---------- +FROM nginx:alpine + +# чистим дефолтку +RUN rm -rf /usr/share/nginx/html/* + +# копируем билд фронта +COPY --from=build /app/dist /usr/share/nginx/html + +# создаём каталог под challenge заранее (на случай, если volume не примонтировался) +RUN mkdir -p /var/www/certbot/.well-known/acme-challenge + +# копируем nginx.conf (он лежит в ./nginx/nginx.conf) +COPY nginx/nginx.conf /etc/nginx/nginx.conf + +EXPOSE 80 443 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..58a608a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Telegram LLM Chat + + +
+ + + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..47da453 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "telegram-llm-chat-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.22.2", + "react-markdown": "^9.0.0", + "remark-gfm": "^4.0.0", + "tailwindcss": "^3.4.1", + "axios": "^1.6.0" + }, + "devDependencies": { + "vite": "^5.2.0", + "@vitejs/plugin-react": "^4.2.1" + } +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..dc8f9a7 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; +import Home from "./pages/Home"; +import UserProfile from "./pages/UserProfile"; +import ChatPage from "./pages/ChatPage"; + +export default function App() { + return ( + +
+
+ + } /> + } /> + } /> + +
+ {/* Навигация снизу */} + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..761945a --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,21 @@ +import axios from "axios"; + +const API_BASE = import.meta.env.VITE_API_URL || "https://llm.byte-mate.xyz"; + +export async function getProfile() { + const resp = await axios.get(`${API_BASE}/api/profile`); + return resp.data; +} + +export async function getChats() { + const resp = await axios.get(`${API_BASE}/api/chats`); + return resp.data; +} + +export async function sendMessage(chatId, message) { + const resp = await axios.post(`${API_BASE}/api/chats/${chatId}/messages`, { + role: "user", + content: message, + }); + return resp.data; +} \ No newline at end of file diff --git a/frontend/src/components/ChatList.jsx b/frontend/src/components/ChatList.jsx new file mode 100644 index 0000000..324ae3e --- /dev/null +++ b/frontend/src/components/ChatList.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +export default function ChatList({ chats }) { + return ( +
+ {chats.map((c) => ( + + 💬 Чат {c} + + ))} + + ➕ Новый чат + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ChatWindow.jsx b/frontend/src/components/ChatWindow.jsx new file mode 100644 index 0000000..8d31056 --- /dev/null +++ b/frontend/src/components/ChatWindow.jsx @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import MessageBubble from "./MessageBubble"; +import { sendMessage } from "../api"; + +export default function ChatWindow({ chatId }) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + + async function handleSend() { + if (!input.trim()) return; + const newMsg = { role: "user", content: input }; + setMessages([...messages, newMsg]); + + const resp = await sendMessage(chatId, input); + setMessages([...messages, newMsg, { role: "assistant", content: resp }]); + setInput(""); + } + + return ( +
+
+ {messages.map((m, i) => ( + + ))} +
+
+ setInput(e.target.value)} + placeholder="Напишите сообщение..." + /> + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/MarkdownRenderer.jsx b/frontend/src/components/MarkdownRenderer.jsx new file mode 100644 index 0000000..f342d28 --- /dev/null +++ b/frontend/src/components/MarkdownRenderer.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +export default function MarkdownRenderer({ children }) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/components/MessageBubble.jsx b/frontend/src/components/MessageBubble.jsx new file mode 100644 index 0000000..4624956 --- /dev/null +++ b/frontend/src/components/MessageBubble.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import MarkdownRenderer from "./MarkdownRenderer"; + +export default function MessageBubble({ role, content }) { + const isUser = role === "user"; + return ( +
+
+ {content} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..9657af5 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); \ No newline at end of file diff --git a/frontend/src/pages/ChatPage.jsx b/frontend/src/pages/ChatPage.jsx new file mode 100644 index 0000000..58a25b7 --- /dev/null +++ b/frontend/src/pages/ChatPage.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import ChatWindow from "../components/ChatWindow"; + +export default function ChatPage() { + const { chatId } = useParams(); + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..eee9cf2 --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,20 @@ +import React, { useEffect, useState } from "react"; +import ChatList from "../components/ChatList"; +import { getProfile } from "../api"; + +export default function Home() { + const [profile, setProfile] = useState(null); + + useEffect(() => { + getProfile().then(setProfile); + }, []); + + if (!profile) return

Загрузка...

; + + return ( +
+

Привет, {profile.username}!

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/UserProfile.jsx b/frontend/src/pages/UserProfile.jsx new file mode 100644 index 0000000..a864812 --- /dev/null +++ b/frontend/src/pages/UserProfile.jsx @@ -0,0 +1,11 @@ +import React from "react"; + +export default function UserProfile() { + return ( +
+

Профиль

+

Имя: demo_user

+

Всего чатов: 3

+
+ ); +} \ No newline at end of file diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/frontend/src/styles/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..aada98e --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://web:8000', + changeOrigin: true + } + } + } +}); \ No newline at end of file diff --git a/getcert.sh b/getcert.sh new file mode 100644 index 0000000..3af613b --- /dev/null +++ b/getcert.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker compose up -d nginx +docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d llm.byte-mate.xyz --email dr.holyblack@gmail.com --agree-tos --no-eff-email \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..223f96a --- /dev/null +++ b/install.sh @@ -0,0 +1,12 @@ +#!/bin/bash +apt-get update +apt-get install ca-certificates curl +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +chmod a+r /etc/apt/keyrings/docker.asc +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ + tee /etc/apt/sources.list.d/docker.list > /dev/null + +apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \ No newline at end of file diff --git a/nginx/certbot/.well-known/acme-challenge/test.txt b/nginx/certbot/.well-known/acme-challenge/test.txt new file mode 100644 index 0000000..ce01362 --- /dev/null +++ b/nginx/certbot/.well-known/acme-challenge/test.txt @@ -0,0 +1 @@ +hello diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..5a31599 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,52 @@ +user nginx; +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; + + server { + listen 80; + server_name llm.byte-mate.xyz; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name llm.byte-mate.xyz; + + ssl_certificate /etc/letsencrypt/live/llm.byte-mate.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/llm.byte-mate.xyz/privkey.pem; + + root /usr/share/nginx/html; + index index.html index.htm; + + location / { + try_files $uri /index.html; + } + + location /api/ { + proxy_pass http://web:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9d9c7d8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +sqlalchemy[asyncio] +asyncpg +python-telegram-bot +openai \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..45813ee --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker compose up --build -d \ No newline at end of file diff --git a/stop.sh b/stop.sh new file mode 100644 index 0000000..a2c900c --- /dev/null +++ b/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker compose down \ No newline at end of file