Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__
.env
.git
.venv
.idea
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.11-slim
LABEL maintainer="Dmitry Titenkov <lt200711@yandex.ru>"
LABEL version="1.0"
LABEL description="Satire Pulp parser"
WORKDIR /app
COPY requirements.txt /app
RUN pip3 install -r /app/requirements.txt --no-cache-dir
COPY . .
115 changes: 71 additions & 44 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging

from config import setup_logger
from dotenv import load_dotenv
from storage import (
from bot_storage import (
get_all_users,
get_last_sent_id,
get_news_after_id,
save_last_sent_news_id,
)
from config import setup_logger
from db_async import AsyncSessionLocal
from dotenv import load_dotenv
from telegram import (
BotCommand,
InlineKeyboardButton,
Expand Down Expand Up @@ -67,22 +68,30 @@ async def send_news(


async def auto_send_news(context: ContextTypes.DEFAULT_TYPE):
users = get_all_users()
if not users:
return
for chat_id in users:
last_id = get_last_sent_id(chat_id)
news_list = get_news_after_id(last_id)
if not news_list:
async with AsyncSessionLocal() as session:
users = await get_all_users(session)
if not users:
return
for news_id, title, image, text, url in news_list:
try:
await send_news(chat_id, context, title, image, text, url)
save_last_sent_news_id(chat_id, news_id)
except Exception as e:
logger.error(
f"Ошибка при автоматической отправке новости: {e}"
)
for chat_id in users:
last_id = await get_last_sent_id(chat_id, session)
news_list = await get_news_after_id(last_id, session)
if not news_list:
continue
for news in news_list:
try:
await send_news(
chat_id,
context,
news.title,
news.image,
news.text,
news.url,
)
await save_last_sent_news_id(chat_id, news.id, session)
except Exception as e:
logger.error(
f"Ошибка при автоматической отправке новости: {e}"
)


async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
Expand Down Expand Up @@ -115,20 +124,28 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
logger.warning(f"callback_query не удалось ответить: {e}")
if query.data == "send_news":
chat_id = query.message.chat_id
last_id = get_last_sent_id(chat_id)
news_list = get_news_after_id(last_id)
if not news_list:
await query.message.reply_text("Новых новостей пока нет 🙁")
logger.info("Новых новостей нет")
return
for news_id, title, image, text, url in news_list:
try:
await send_news(chat_id, context, title, image, text, url)
save_last_sent_news_id(chat_id, news_id)
except Exception as e:
logger.error(
f"Ошибка при отправке новости '{title[:25]}': {e}"
)
async with AsyncSessionLocal() as session:
last_id = await get_last_sent_id(chat_id, session)
news_list = await get_news_after_id(last_id, session)
if not news_list:
await query.message.reply_text("Новых новостей пока нет 🙁")
logger.info("Новых новостей нет")
return
for news in news_list:
try:
await send_news(
chat_id,
context,
news.title,
news.image,
news.text,
news.url,
)
await save_last_sent_news_id(chat_id, news.id, session)
except Exception as e:
logger.error(
f"Ошибка при отправке новости '{news.title[:25]}': {e}"
)
elif query.data == "help":
help_text = "Нажмите 📰 'Показать новости', чтобы получить новости."
await query.message.reply_text(help_text)
Expand All @@ -138,18 +155,28 @@ async def show_news_command(
update: Update, context: ContextTypes.DEFAULT_TYPE
):
chat_id = update.message.chat_id
last_id = get_last_sent_id(chat_id)
news_list = get_news_after_id(last_id)
if not news_list:
await update.message.reply_text("Новостей пока нет 🙁")
logger.info("Новостей нет")
return
try:
for news_id, title, image, text, url in news_list:
await send_news(chat_id, context, title, image, text, url)
save_last_sent_news_id(chat_id, news_id)
except Exception as e:
logger.error(f"Ошибка при отправке новости '{title[:25]}': {e}")
async with AsyncSessionLocal() as session:
last_id = await get_last_sent_id(chat_id, session)
news_list = await get_news_after_id(last_id, session)
if not news_list:
await update.message.reply_text("Новостей пока нет 🙁")
logger.info("Новостей нет")
return
try:
for news in news_list:
await send_news(
chat_id,
context,
news.title,
news.image,
news.text,
news.url,
)
await save_last_sent_news_id(chat_id, news.id, session)
except Exception as e:
logger.error(
f"Ошибка при отправке новости '{news.title[:25]}': {e}"
)


async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
Expand Down
79 changes: 79 additions & 0 deletions bot_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import logging

from models import LastSentNews, News
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)


async def get_news_after_id(last_id: int, session: AsyncSession, n=10):
"""
Получение новостей если если id новости меньше чем last_id отправленной новости,
либо получение 10 последних новостей если last_id = 0
"""
try:
if last_id == 0:
news = await session.execute(
select(News)
.where(News.id > last_id)
.order_by(News.id.desc())
.limit(n)
)
return news.scalars().all()[::-1]
else:
news = await session.execute(
select(News).where(News.id > last_id).order_by(News.id.asc())
)
return news.scalars().all()
except SQLAlchemyError as e:
logger.error(f"Ошибка при получении списка новостей: {e}")
raise


async def get_last_sent_id(chat_id: int, session: AsyncSession):
"""Получение last_id последней отправленной новости"""
try:
last_sent = await session.execute(
select(LastSentNews).where(LastSentNews.chat_id == chat_id)
)
last_sent_id = last_sent.scalar_one_or_none()
if last_sent_id:
return last_sent_id.last_news_id
else:
return 0
except SQLAlchemyError as e:
logger.error(
f"Ошибка при получении id последней отправленной новости: {e}"
)


async def save_last_sent_news_id(
chat_id: int, last_id: int, session: AsyncSession
):
"""Сохранение last_id последней отправленной новости"""
try:
last_sent = await session.execute(
select(LastSentNews).where(LastSentNews.chat_id == chat_id)
)
last_sent = last_sent.scalar_one_or_none()
if last_sent:
last_sent.last_news_id = last_id
else:
last_sent = LastSentNews(chat_id=chat_id, last_news_id=last_id)
session.add(last_sent)
await session.commit()
except SQLAlchemyError as e:
await session.rollback()
logger.error(f"Ошибка при сохранении last_news_id: {e}")
raise


async def get_all_users(session: AsyncSession):
"""Получение id юзера"""
try:
users = await session.execute(select(LastSentNews.chat_id))
return users.scalars().all()
except SQLAlchemyError as e:
logger.error(f"Ошибка получения chat_id пользователей: {e}")
12 changes: 12 additions & 0 deletions db_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os

from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

load_dotenv()


engine = create_async_engine(os.getenv("DATABASE_URL_ASYNC"), echo=False)


AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
12 changes: 12 additions & 0 deletions db_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os

from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

load_dotenv()


engine = create_engine(os.getenv("DATABASE_URL_SYNC"))

SessionLocal = sessionmaker(engine)
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: '3.8'

services:
db:
image: postgres:15.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
ports:
- "5432:5432"
env_file:
- ./.env

bot:
build: .
restart: always
depends_on:
- db
env_file:
- ./.env
command: python3 main.py

scheduler:
build: .
restart: always
command: python3 scheduler.py
depends_on:
- db
- bot
env_file:
- ./.env

volumes:
postgres_data:
22 changes: 22 additions & 0 deletions init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import asyncio
import logging

from config import setup_logger
from db_async import engine
from dotenv import load_dotenv
from models import Base, LastSentNews, News # noqa

load_dotenv()

setup_logger()
logger = logging.getLogger(__name__)


async def init():
"""Создание таблиц в базе"""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("...Таблицы созданы...")


asyncio.run(init())
2 changes: 0 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
)
from config import setup_logger
from dotenv import load_dotenv
from storage import init_db
from telegram.ext import (
ApplicationBuilder,
CallbackQueryHandler,
Expand All @@ -26,7 +25,6 @@


def main():
init_db()
app = ApplicationBuilder().token(os.getenv("TELEGRAM_TOKEN")).build()
app.job_queue.run_repeating(auto_send_news, interval=600, first=10)
app.add_handler(CommandHandler("start", menu))
Expand Down
30 changes: 30 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from datetime import datetime

from sqlalchemy import BigInteger, DateTime, Integer, String, Text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
pass


class News(Base):
__tablename__ = "news"

id: Mapped[int] = mapped_column(primary_key=True)
url: Mapped[str] = mapped_column(String, unique=True, nullable=False)
title: Mapped[str | None] = mapped_column(String, nullable=True)
image: Mapped[str | None] = mapped_column(String, nullable=True)
text: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=func.now(), nullable=False
)


class LastSentNews(Base):
__tablename__ = "last_sent_news"

chat_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
last_news_id: Mapped[int] = mapped_column(
Integer, nullable=True, default=0
)
Loading