Skip to content

Commit f58a162

Browse files
authored
Add files via upload
1 parent 93b669d commit f58a162

22 files changed

+692
-0
lines changed

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM python:3.12
2+
WORKDIR /app
3+
COPY . /app/
4+
RUN pip install -r requirements.txt
5+
ENTRYPOINT python main.py

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Музейный Бот для города Екатеринбурга
2+
3+
Добро пожаловать в **Музейный Бот**, телеграмм-бот, предназначенный для помощи в поиске музеев в городе Екатеринбург.
4+
5+
Этот бот написан на Python с использованием библиотек `aiogram`, `geopy`, `requests`, `beautifulsoup4`, `SQLAlchemy`.
6+
Кроме того использованы **Базы Данных** для хранения данных (`PostgreSQL`)
7+
8+
## Особенности
9+
10+
- **Поиск по местоположению**: Поделитесь своим местоположением и узнайте 3 ближайших к вам музея в Екатеринбурге.
11+
- **Поиск по интересам**: Выберите из различных категорий, таких как История, Архитектура, Искусство, Литература, Музыка, Наука и техника, Природа и Этнография.
12+
- **Рандомный музей**: Возможность рандомно выбрать музей в городе.
13+
- **Избранное**: Добавляйте понравившееся музеи в избранное, чтобы быстрее находить их в боте.
14+
15+
## Установка
16+
17+
1. **Клонируйте репозиторий**:
18+
```bash
19+
git clone https://github.com/korjeek/MuseumBot.git
20+
cd MuseumBot
21+
```
22+
23+
2. **Установите необходимые пакеты**:
24+
```bash
25+
pip install -r requirements.txt
26+
```
27+
28+
3. **Настройте токен ваши переменные окружения `.env`**:
29+
- Создайте файл `.env`.
30+
- Впишите туда свой токен для ТГ бота и необходимые для БД переменные (`port`, `password`, `user` и т.д.)
31+
32+
33+
## Использование
34+
35+
**1 Способ**:
36+
37+
Чтобы запустить бота, просто выполните скрипт:
38+
39+
```bash
40+
python3 main.py
41+
```
42+
43+
**2 Способ**:
44+
45+
Запуск бота можно сделать через `Docker`. `DockerFile` и `docker-compose.yml` приложены. Необходимо просто запустить `Docker` и
46+
вписать в терминал команду:
47+
48+
```bash
49+
docker compose up
50+
```
51+
52+
## Требования
53+
54+
- `Python` версии не ниже **3.12.0**
55+
- `SQLAlchemy` версии не ниже **2.0.32**
56+
- `geopy` версии не ниже **2.4.1**
57+
- `aiogram` версии не ниже **3.12.0**
58+
- `requests` версии не ниже **2.32.3**
59+
- `beautifulsoup4` версии не ниже **4.12.3**
60+
61+
Сейчас можно протестировать работу телеграм бота по ссылке: https://t.me/museum_ekb_bot
8.49 KB
Binary file not shown.
1.61 KB
Binary file not shown.
3.81 KB
Binary file not shown.
8.07 KB
Binary file not shown.

app/database/models.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from dotenv import load_dotenv
2+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
3+
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
4+
from sqlalchemy import String, BigInteger, ForeignKey, Column, URL
5+
import os
6+
7+
load_dotenv()
8+
postgres_url = URL.create(
9+
"postgresql+asyncpg",
10+
username=os.getenv('POSTGRES_USER'),
11+
password=os.getenv('POSTGRES_PASSWORD'),
12+
host=os.getenv('POSTGRES_HOST'),
13+
database=os.getenv('POSTGRES_DB'),
14+
port=os.getenv('POSTGRES_PORT')
15+
)
16+
17+
engine = create_async_engine(url=postgres_url)
18+
async_session = async_sessionmaker(engine)
19+
20+
21+
class Base(AsyncAttrs, DeclarativeBase):
22+
pass
23+
24+
25+
class User(Base):
26+
__tablename__ = 'users'
27+
28+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
29+
user_id = Column(BigInteger, unique=True)
30+
31+
32+
class Museum(Base):
33+
__tablename__ = 'museums'
34+
35+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
36+
name: Mapped[str] = mapped_column(String(80))
37+
category: Mapped[int] = mapped_column(ForeignKey('categories.id'))
38+
latitude: Mapped[str] = mapped_column(String(10))
39+
longitude: Mapped[str] = mapped_column(String(10))
40+
site: Mapped[str] = mapped_column(String(110))
41+
request_site: Mapped[str] = mapped_column(String(120))
42+
43+
44+
class Category(Base):
45+
__tablename__ = 'categories'
46+
47+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
48+
name: Mapped[str] = mapped_column(String(20))
49+
50+
51+
class Favorite(Base):
52+
__tablename__ = 'favorite'
53+
54+
id: Mapped[int] = mapped_column(primary_key=True)
55+
user: Mapped[int] = mapped_column(ForeignKey('users.id'))
56+
museum: Mapped[int] = mapped_column(ForeignKey('museums.id'))
57+
58+
59+
async def create_tables():
60+
async with engine.begin() as conn:
61+
await conn.run_sync(Base.metadata.create_all)

app/database/requests.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from app.database.models import async_session, Category, Museum, Favorite, User
2+
from sqlalchemy import select
3+
from geopy.distance import geodesic
4+
5+
6+
# =======================================Пользователи======================================
7+
async def set_user(user_id):
8+
async with async_session() as session:
9+
user = await session.scalar(select(User).where(User.user_id == user_id))
10+
11+
if not user:
12+
session.add(User(user_id=user_id))
13+
await session.commit()
14+
15+
16+
async def get_user_id(user_id):
17+
async with async_session() as session:
18+
return await session.scalar(select(User.id).where(User.user_id == user_id))
19+
20+
21+
# ========================================Категории========================================
22+
async def get_categories():
23+
async with async_session() as session:
24+
return await session.scalars(select(Category))
25+
26+
27+
async def get_category_name(category_id: int):
28+
async with async_session() as session:
29+
category_name = await session.scalar(select(Category).where(Category.id == category_id))
30+
return category_name.name
31+
32+
33+
# ==========================================Музеи==========================================
34+
async def get_museums(category_id: int):
35+
async with async_session() as session:
36+
return await session.scalars(select(Museum).where(Museum.category == category_id))
37+
38+
39+
async def get_museum(museum_id):
40+
async with async_session() as session:
41+
return await session.scalar(select(Museum).where(Museum.id == museum_id))
42+
43+
44+
async def get_local_museums(user_location: tuple[float, float]):
45+
async with async_session() as session:
46+
museums = list(await session.scalars(select(Museum)))
47+
museums.sort(key=lambda museum: get_distance(user_location, (museum.latitude, museum.longitude)))
48+
return museums[:3]
49+
50+
51+
def get_distance(user_location: tuple[float, float], museum_location: tuple[float, float]) -> float:
52+
return geodesic(museum_location, user_location).meters
53+
54+
55+
# ========================================Избранное========================================
56+
async def check_favorite(user_id: int, museum: int):
57+
async with async_session() as session:
58+
user = await get_user_id(user_id)
59+
favorite = await session.scalar(select(Favorite).where(Favorite.museum == museum and Favorite.user == user))
60+
return True if favorite else False
61+
62+
63+
async def change_favorite(user_id: int, museum: int):
64+
async with async_session() as session:
65+
user = await get_user_id(user_id)
66+
favorite = await session.scalar(select(Favorite).where(Favorite.museum == museum and Favorite.user == user))
67+
68+
if not favorite:
69+
session.add(Favorite(user=user, museum=museum))
70+
else:
71+
await session.delete(favorite)
72+
await session.commit()
73+
74+
75+
async def get_favorite_museums(user_id: int):
76+
async with async_session() as session:
77+
user = await get_user_id(user_id)
78+
favorite_museums = await session.scalars(select(Favorite.museum).where(Favorite.user == user))
79+
80+
return [await get_museum(museum_id) for museum_id in favorite_museums]

app/handlers.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from aiogram import Router, F
2+
from aiogram.filters import CommandStart, Command
3+
from aiogram.types import Message, CallbackQuery, ContentType, ReplyKeyboardRemove
4+
from app.messages.keyboards import Keyboard, CategoryCallback, RandomCallback, LocalCallback, FavoriteCallback
5+
from app.messages import text
6+
from app.database.requests import change_favorite, set_user
7+
from app.messages.creator import Creator
8+
from random import randint
9+
10+
router = Router()
11+
kbd = Keyboard()
12+
creator = Creator()
13+
14+
15+
# ========================================Команды==========================================
16+
@router.message(CommandStart())
17+
async def start(message: Message):
18+
await set_user(message.from_user.id)
19+
await message.answer(text=text.START_MESSAGE)
20+
21+
22+
@router.message(Command('choose'))
23+
async def choose(message: Message):
24+
await message.answer(text=text.CHOOSE_MESSAGE, reply_markup=await kbd.categories())
25+
26+
27+
@router.message(Command('random'))
28+
async def random(message: Message):
29+
keyboard, museum_data = await creator.random_museum_msg(message.from_user.id, randint(1, 37))
30+
await message.answer(text=museum_data, reply_markup=keyboard, parse_mode='html')
31+
32+
33+
@router.message(Command('locate'))
34+
async def locate(message: Message):
35+
await message.answer(text=text.LOCATION_REQ_MESSAGE, reply_markup=await kbd.location_kbd())
36+
37+
38+
@router.message(Command('favorites'))
39+
async def favorites(message: Message):
40+
keyboard, museum_data = await creator.favorite_museum_msg(FavoriteCallback(page=1), message.from_user.id)
41+
42+
if keyboard is None:
43+
await message.answer(text=text.FAVORITE_EMPTY_MESSAGE)
44+
else:
45+
await message.answer(text=museum_data, reply_markup=keyboard, parse_mode='html')
46+
47+
48+
# ==================================Callback-Обработчики===================================
49+
@router.callback_query(CategoryCallback.filter())
50+
async def category_callback_query(call: CallbackQuery, callback_data: CategoryCallback) -> None:
51+
# Нужно ли изменить состояние музея на 'избранное / не избранное'
52+
if callback_data.change_favorite_state:
53+
await change_favorite(call.from_user.id, callback_data.museum)
54+
# Нужно ли вернуться в начальное меню
55+
if callback_data.close:
56+
await call.message.edit_text(text=text.CHOOSE_MESSAGE, reply_markup=await kbd.categories())
57+
# Иначе, создание новой страницы музея
58+
else:
59+
keyboard, museum_data = await creator.category_museum_msg(callback_data, call.from_user.id)
60+
await call.message.edit_text(text=museum_data, reply_markup=keyboard, parse_mode='html')
61+
62+
63+
@router.callback_query(RandomCallback.filter())
64+
async def back_callback_query(call: CallbackQuery, callback_data: CategoryCallback) -> None:
65+
# Нужно ли изменить состояние музея на 'избранное / не избранное'
66+
if callback_data.change_favorite_state:
67+
await change_favorite(call.from_user.id, callback_data.museum)
68+
69+
# Создание новой страницы музея
70+
keyboard, museum_data = await creator.random_museum_msg(call.from_user.id, callback_data.museum)
71+
await call.message.edit_text(text=museum_data, reply_markup=keyboard, parse_mode='html')
72+
73+
74+
@router.message(F.content_type == ContentType.LOCATION)
75+
async def locate_museums(message: Message):
76+
# Удаление кнопки 'запроса'
77+
await message.answer(text=text.LOCATE_MESSAGE, reply_markup=ReplyKeyboardRemove())
78+
79+
# Создание новой страницы музея
80+
callback_data = LocalCallback(latitude=message.location.latitude, longitude=message.location.longitude, page=1)
81+
keyboard, museum_data = await creator.local_museum_msg(callback_data, message.from_user.id)
82+
await message.answer(text=museum_data, reply_markup=keyboard, parse_mode='html')
83+
84+
85+
@router.callback_query(LocalCallback.filter())
86+
async def geo_callback_query(call: CallbackQuery, callback_data: LocalCallback):
87+
# Нужно ли изменить состояние музея на 'избранное / не избранное'
88+
if callback_data.change_favorite_state:
89+
await change_favorite(call.from_user.id, callback_data.museum)
90+
91+
# Создание новой страницы музея
92+
keyboard, museum_data = await creator.local_museum_msg(callback_data, call.from_user.id)
93+
await call.message.edit_text(text=museum_data, reply_markup=keyboard, parse_mode='html')
94+
95+
96+
@router.callback_query(FavoriteCallback.filter())
97+
async def favorite_callback_query(call: CallbackQuery, callback_data: FavoriteCallback):
98+
# Нужно ли изменить состояние музея на 'избранное / не избранное'
99+
if callback_data.change_favorite_state:
100+
await change_favorite(call.from_user.id, callback_data.museum)
101+
102+
# Создание новой страницы музея
103+
keyboard, museum_data = await creator.favorite_museum_msg(callback_data, call.from_user.id)
104+
105+
if keyboard is None:
106+
await call.message.edit_text(text=text.FAVORITE_EMPTY_MESSAGE)
107+
else:
108+
await call.message.edit_text(text=museum_data, reply_markup=keyboard, parse_mode='html')
8.84 KB
Binary file not shown.

0 commit comments

Comments
 (0)