update
This commit is contained in:
parent
ee58d3a7a4
commit
9937d4e250
2
.env.example
Normal file
2
.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxx
|
||||||
|
WEBHOOK_URL=https://ваш-домен.ru
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
logs/
|
||||||
|
|
@ -8,14 +8,11 @@ ENV PYTHONFAULTHANDLER=1 \
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||||
PIP_DEFAULT_TIMEOUT=100
|
PIP_DEFAULT_TIMEOUT=100
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
RUN pip install --upgrade pip && \
|
RUN pip install --upgrade pip && \
|
||||||
pip install --no-cache-dir --disable-pip-version-check -r requirements.txt
|
pip install --no-cache-dir --disable-pip-version-check -r requirements.txt
|
||||||
|
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -32,10 +32,10 @@ Telegram-бот, который автоматически парсит и от
|
||||||
### 1. Клонируйте репозиторий
|
### 1. Клонируйте репозиторий
|
||||||
|
|
||||||
### 2. Настройте конфигурацию
|
### 2. Настройте конфигурацию
|
||||||
Отредактируйте файл `config.py`:
|
Скопируйте `.env.example` в `.env` и отредактируйте.
|
||||||
```python
|
```python
|
||||||
token = 'ВАШ_TELEGRAM_BOT_TOKEN'
|
BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxx
|
||||||
URL_APP = 'https://ваш-домен.ru'
|
WEBHOOK_URL=https://ваш-домен.ru
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Соберите и запустите через Docker
|
### 3. Соберите и запустите через Docker
|
||||||
|
|
@ -48,25 +48,12 @@ docker run -d \
|
||||||
mrsk_bot
|
mrsk_bot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Или через docker compose
|
||||||
|
|
||||||
> Убедитесь, что ваш сервер доступен по `URL_APP` и настроен reverse proxy (например, Nginx) на порт 5000.
|
> Убедитесь, что ваш сервер доступен по `URL_APP` и настроен reverse proxy (например, Nginx) на порт 5000.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── bot.py # Основной файл бота
|
|
||||||
├── main.py # Парсер данных с сайта МРСК
|
|
||||||
├── config.py # Конфигурация (токен, URL, API-эндпоинты)
|
|
||||||
├── requirements.txt # Зависимости Python
|
|
||||||
├── Dockerfile # Для сборки образа
|
|
||||||
├── logs/ # Директория для логов (бот.log, parser.log, users.log)
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Обратная связь
|
## Обратная связь
|
||||||
|
|
||||||
Если у вас есть вопросы, предложения или вы нашли баг — пишите: [@pikusQQ](https://t.me/pikusQQ)
|
Если у вас есть вопросы, предложения или вы нашли баг — пишите: [@pikusQQ](https://t.me/pikusQQ)
|
||||||
|
|
|
||||||
28
bot.py
28
bot.py
|
|
@ -1,13 +1,18 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
|
||||||
from aiogram import Bot, Dispatcher, executor, types
|
from aiogram import Bot, Dispatcher, executor, types
|
||||||
from aiogram.dispatcher.filters import Text
|
from aiogram.dispatcher.filters import Text
|
||||||
import config
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from main import start_parser
|
from main import start_parser
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
filename="logs/bot.log",
|
filename="logs/bot.log",
|
||||||
|
|
@ -16,11 +21,19 @@ logging.basicConfig(
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
|
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
|
||||||
|
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise ValueError("❌ Переменная BOT_TOKEN не задана в .env")
|
||||||
|
if not WEBHOOK_URL:
|
||||||
|
raise ValueError("❌ Переменная WEBHOOK_URL не задана в .env")
|
||||||
|
|
||||||
TZ = pytz.timezone('Europe/Moscow')
|
TZ = pytz.timezone('Europe/Moscow')
|
||||||
USER_LOG = Path("logs/users.log")
|
USER_LOG = Path("logs/users.log")
|
||||||
UPDATE_INTERVAL = 300 # 5 минут в секундах
|
UPDATE_INTERVAL = 300 # 5 минут
|
||||||
|
|
||||||
bot = Bot(token=config.token)
|
bot = Bot(token=BOT_TOKEN)
|
||||||
dp = Dispatcher(bot)
|
dp = Dispatcher(bot)
|
||||||
|
|
||||||
async def periodic_update():
|
async def periodic_update():
|
||||||
|
|
@ -40,7 +53,6 @@ async def cmd_start(message: types.Message):
|
||||||
keyboard.add(*buttons)
|
keyboard.add(*buttons)
|
||||||
await message.answer('Выберите тип отключений:', reply_markup=keyboard)
|
await message.answer('Выберите тип отключений:', reply_markup=keyboard)
|
||||||
|
|
||||||
# Утилита для отправки файла
|
|
||||||
async def send_file_content(message: types.Message, file_path: Path, empty_msg: str, log_msg: str):
|
async def send_file_content(message: types.Message, file_path: Path, empty_msg: str, log_msg: str):
|
||||||
await message.answer("Обновляю...")
|
await message.answer("Обновляю...")
|
||||||
|
|
||||||
|
|
@ -101,12 +113,10 @@ async def info(message: types.Message):
|
||||||
async def fallback(message: types.Message):
|
async def fallback(message: types.Message):
|
||||||
await message.answer("Нажмите /start для начала работы.")
|
await message.answer("Нажмите /start для начала работы.")
|
||||||
|
|
||||||
# Webhook handlers
|
|
||||||
async def on_startup(dp):
|
async def on_startup(dp):
|
||||||
await bot.set_webhook(config.URL_APP)
|
await bot.set_webhook(WEBHOOK_URL)
|
||||||
# Запуск фоновой задачи после старта
|
|
||||||
asyncio.create_task(periodic_update())
|
asyncio.create_task(periodic_update())
|
||||||
logger.info("Webhook установлен. Фоновое обновление запущено.")
|
logger.info(f"Webhook установлен на {WEBHOOK_URL}. Фоновое обновление запущено.")
|
||||||
|
|
||||||
async def on_shutdown(dp):
|
async def on_shutdown(dp):
|
||||||
await bot.delete_webhook()
|
await bot.delete_webhook()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
main_url = 'https://www.mrsk-1.ru/customers/customer-service/power-outage/ajax.php?request='
|
|
||||||
districtCode = '0ECC8970-2702-4C14-9E4D-956606EFFEE3'
|
|
||||||
localityCode = '02E9C019-AB4D-4FA0-928E-D6C0A41DC256'
|
|
||||||
url_vnereglament = f'{main_url}3®ionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}'
|
|
||||||
url_avar = f'{main_url}4®ionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}&Street=-'
|
|
||||||
url_plan = f'{main_url}2®ionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}'
|
|
||||||
token = ''
|
|
||||||
URL_APP = ''
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mrsk-bot:
|
power-outage-bot:
|
||||||
build: .
|
build: .
|
||||||
container_name: mrsk-bot
|
container_name: power-outage-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "5002:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
|
- ./.env:/app/.env:ro
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
networks:
|
|
||||||
- bot-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
bot-network:
|
|
||||||
driver: bridge
|
|
||||||
31
main.py
31
main.py
|
|
@ -1,3 +1,5 @@
|
||||||
|
# main.py
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -5,8 +7,6 @@ from pathlib import Path
|
||||||
from urllib3.exceptions import InsecureRequestWarning
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
import config
|
|
||||||
|
|
||||||
urllib3.disable_warnings(InsecureRequestWarning)
|
urllib3.disable_warnings(InsecureRequestWarning)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -15,30 +15,28 @@ logging.basicConfig(
|
||||||
format="%(asctime)s - %(module)s - %(levelname)s - %(funcName)s: %(lineno)d - %(message)s",
|
format="%(asctime)s - %(module)s - %(levelname)s - %(funcName)s: %(lineno)d - %(message)s",
|
||||||
datefmt='%H:%M:%S',
|
datefmt='%H:%M:%S',
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MAIN_URL = 'https://www.mrsk-1.ru/customers/customer-service/power-outage/ajax.php?request='
|
||||||
|
DISTRICT_CODE = '0ECC8970-2702-4C14-9E4D-956606EFFEE3'
|
||||||
|
LOCALITY_CODE = '02E9C019-AB4D-4FA0-928E-D6C0A41DC256'
|
||||||
|
URL_PLAN = f'{MAIN_URL}2®ionCode=31&districtCode={DISTRICT_CODE}&locality=Белгород&localityCode={LOCALITY_CODE}'
|
||||||
|
URL_VNEREGLAMENT = f'{MAIN_URL}3®ionCode=31&districtCode={DISTRICT_CODE}&locality=Белгород&localityCode={LOCALITY_CODE}'
|
||||||
|
URL_AVAR = f'{MAIN_URL}4®ionCode=31&districtCode={DISTRICT_CODE}&locality=Белгород&localityCode={LOCALITY_CODE}&Street=-'
|
||||||
|
|
||||||
PLAN_FILE = Path("plan.txt")
|
PLAN_FILE = Path("plan.txt")
|
||||||
VN_FILE = Path("vnereglament.txt")
|
VN_FILE = Path("vnereglament.txt")
|
||||||
AVAR_FILE = Path("avar.txt")
|
AVAR_FILE = Path("avar.txt")
|
||||||
|
|
||||||
def clean_street(street: str) -> str:
|
def clean_street(street: str) -> str:
|
||||||
"""Очищает и форматирует строку с адресами."""
|
|
||||||
street = street.replace('Белгород г; ', '').replace('\n\n', '\n').strip()
|
street = street.replace('Белгород г; ', '').replace('\n\n', '\n').strip()
|
||||||
lines = [line.strip() for line in street.split('\n') if line.strip()]
|
lines = [line.strip() for line in street.split('\n') if line.strip()]
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
def format_time(dt_str: str) -> str:
|
def format_time(dt_str: str) -> str:
|
||||||
"""Преобразует '2025-10-02T09:00:00' → '02.10.2025 09:00'."""
|
return dt_str.replace('T', ' ')
|
||||||
try:
|
|
||||||
return dt_str.replace('T', ' ')
|
|
||||||
except:
|
|
||||||
return dt_str
|
|
||||||
|
|
||||||
def parse_and_save(data, file_path: Path, mode: str):
|
def parse_and_save(data, file_path: Path, mode: str):
|
||||||
"""
|
|
||||||
mode: 'plan', 'vnereglament', 'avar'
|
|
||||||
"""
|
|
||||||
if not data or data == 0:
|
if not data or data == 0:
|
||||||
file_path.write_text("[Нет отключений]", encoding='utf-8')
|
file_path.write_text("[Нет отключений]", encoding='utf-8')
|
||||||
return
|
return
|
||||||
|
|
@ -61,7 +59,7 @@ def parse_and_save(data, file_path: Path, mode: str):
|
||||||
|
|
||||||
def get_plan():
|
def get_plan():
|
||||||
try:
|
try:
|
||||||
r = requests.get(url=config.url_plan, verify=False, timeout=10)
|
r = requests.get(url=URL_PLAN, verify=False, timeout=10)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
parse_and_save(data, PLAN_FILE, 'plan')
|
parse_and_save(data, PLAN_FILE, 'plan')
|
||||||
|
|
@ -72,7 +70,7 @@ def get_plan():
|
||||||
|
|
||||||
def get_vnereglament():
|
def get_vnereglament():
|
||||||
try:
|
try:
|
||||||
r = requests.get(url=config.url_vnereglament, verify=False, timeout=10)
|
r = requests.get(url=URL_VNEREGLAMENT, verify=False, timeout=10)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
parse_and_save(data, VN_FILE, 'vnereglament')
|
parse_and_save(data, VN_FILE, 'vnereglament')
|
||||||
|
|
@ -83,7 +81,7 @@ def get_vnereglament():
|
||||||
|
|
||||||
def get_avar():
|
def get_avar():
|
||||||
try:
|
try:
|
||||||
r = requests.get(url=config.url_avar, verify=False, timeout=10)
|
r = requests.get(url=URL_AVAR, verify=False, timeout=10)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
parse_and_save(data, AVAR_FILE, 'avar')
|
parse_and_save(data, AVAR_FILE, 'avar')
|
||||||
|
|
@ -97,6 +95,3 @@ def start_parser():
|
||||||
get_vnereglament()
|
get_vnereglament()
|
||||||
get_avar()
|
get_avar()
|
||||||
logger.info("Парсер отработал")
|
logger.info("Парсер отработал")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
start_parser()
|
|
||||||
|
|
@ -14,3 +14,4 @@ pytz==2024.1
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
yarl==1.9.4
|
yarl==1.9.4
|
||||||
|
python-dotenv==1.0.1
|
||||||
Loading…
Reference in New Issue
Block a user