commit ecfd86bead3ef3b3891f32e9cec9cbb9756fdd8e Author: Ivan Ashikhmin Date: Sat Apr 27 21:42:08 2024 +0400 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f563ec7 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +token= +admin_id= +notion_token= +imgur_client_id= +database_id= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0bfe01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +venv +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7719379 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /code + +COPY requirements.txt /code +RUN pip install --upgrade pip && pip install -r requirements.txt + +COPY . /code + +CMD [ "python", "./main.py" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..89c1061 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +## Notion Bot + +Telegram-бот для пересылки сообщений в базу-данных Notion. + +## Особенности +- Пересылка сообщения в Notion +- Получение ссылок из сообщения для занесения в URL-столбцы +- Получение изображения из сообщения для сохранения в Notion. Для отправки сообщения используется хостинг изображений Imgur. + +## Запуск +Необходимо переименовать `.env.example` в `.env` и прописать соответствующие данные. +Для запуска достаточно выполнить команду `docker compose up -d`. \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/handlers/events.py b/app/handlers/events.py new file mode 100644 index 0000000..d4daf3a --- /dev/null +++ b/app/handlers/events.py @@ -0,0 +1,10 @@ +from app.settings import bot, secrets +from app import views + + +async def start_bot(): + await bot.send_message(secrets.admin_id, views.start_bot_message()) + + +async def stop_bot(): + await bot.send_message(secrets.admin_id, views.stop_bot_message()) diff --git a/app/handlers/message.py b/app/handlers/message.py new file mode 100644 index 0000000..d518684 --- /dev/null +++ b/app/handlers/message.py @@ -0,0 +1,9 @@ +from aiogram.types import Message, MessageEntity, PhotoSize, ReactionTypeEmoji + +from app.utils.send_to_notion import send_to_notion + + +async def parse_message(message: Message): + await send_to_notion(message) + + await message.react([ReactionTypeEmoji(emoji="👌")]) diff --git a/app/handlers/simple.py b/app/handlers/simple.py new file mode 100644 index 0000000..9c1a1d3 --- /dev/null +++ b/app/handlers/simple.py @@ -0,0 +1,6 @@ +from aiogram.types import Message +from app import views + + +async def start_command(message: Message): + await message.answer(views.start_text()) diff --git a/app/middlewares/__init__.py b/app/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/middlewares/admin_middleware.py b/app/middlewares/admin_middleware.py new file mode 100644 index 0000000..30a18ba --- /dev/null +++ b/app/middlewares/admin_middleware.py @@ -0,0 +1,18 @@ +from typing import Callable, Dict, Any, Awaitable + +from aiogram import BaseMiddleware +from aiogram.types import TelegramObject, User + +from app.settings import secrets + + +class AdminMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: Dict[str, Any], + ) -> Any: + user: User = data.get("event_from_user") + if user.id == secrets.admin_id: + return await handler(event, data) diff --git a/app/middlewares/album_middleware.py b/app/middlewares/album_middleware.py new file mode 100644 index 0000000..83eb218 --- /dev/null +++ b/app/middlewares/album_middleware.py @@ -0,0 +1,33 @@ +import asyncio +from typing import Dict, List, Union, Callable, Any, Awaitable + +from aiogram import BaseMiddleware +from aiogram.types import Message, TelegramObject + +DEFAULT_DELAY = 0.6 + + +class MediaGroupMiddleware(BaseMiddleware): + ALBUM_DATA: Dict[str, List[Message]] = {} + + def __init__(self, delay: Union[int, float] = DEFAULT_DELAY): + self.delay = delay + + async def __call__( + self, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: Message, + data: Dict[str, Any], + ) -> Any: + if not event.media_group_id: + return await handler(event, data) + + try: + self.ALBUM_DATA[event.media_group_id].append(event) + return # Don't propagate the event + except KeyError: + self.ALBUM_DATA[event.media_group_id] = [event] + await asyncio.sleep(self.delay) + data["album"] = self.ALBUM_DATA.pop(event.media_group_id) + + return await handler(event, data) diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..9fe89b5 --- /dev/null +++ b/app/settings.py @@ -0,0 +1,25 @@ +import os +from typing import Union + +from aiogram import Bot +from notion_client import AsyncClient +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class Secrets(BaseSettings): + token: SecretStr + admin_id: Union[SecretStr.get_secret_value, int] + notion_token: SecretStr + imgur_client_id: SecretStr + database_id: SecretStr + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +secrets = Secrets() + +notion = AsyncClient(auth=secrets.notion_token.get_secret_value()) +bot = Bot(token=secrets.token.get_secret_value()) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/send_to_notion.py b/app/utils/send_to_notion.py new file mode 100644 index 0000000..0536739 --- /dev/null +++ b/app/utils/send_to_notion.py @@ -0,0 +1,79 @@ +import os +import re +from datetime import datetime, timezone +from os import path + +from imgur_python import Imgur +from aiogram.types import Message, MessageEntity, PhotoSize + +from app.settings import notion, bot, secrets + + +async def send_to_notion(message: Message): + pattern = r"(https?://[^\s]+|t\.me/[^\s]+)" + image_url = None + if message.caption: + links2 = re.findall(pattern, message.caption) + text: str = message.caption + photos: PhotoSize = [photo.file_id for photo in message.photo] + links: MessageEntity = [ + link.url for link in message.caption_entities if link.type == "text_link" + ] + if photos: + file_name = f"images/{photos[0]}.jpg" + await bot.download(message.photo[-1], destination=file_name) + imgur_client = Imgur( + {"client_id": secrets.imgur_client_id.get_secret_value()} + ) + image = imgur_client.image_upload( + path.realpath(file_name), "Untitled", "My first image upload" + ) + image_url = image["response"]["data"]["link"] + os.remove(file_name) + else: + text: str = message.text + links2 = re.findall(pattern, message.text) + photos = [] + links: MessageEntity = ( + [link.url for link in message.entities if link.type == "text_link"] + if message.entities + else [] + ) + if links2: + links.extend(links2) + links = set(links) + properties = { + "Name": {"title": [{"text": {"content": text[: text.index("\n")]}}]}, + "Text": {"rich_text": [{"text": {"content": text}}]}, + "Added at": { + "date": { + "start": datetime.now().astimezone(timezone.utc).isoformat(), + "end": None, + } + }, + } + for i, link in enumerate(links, start=1): + if i > 4: + break + properties[f"Link{i}"] = {"url": link} + cover = None + if photos: + properties["Image"] = { + "files": [ + { + "name": "image.jpg", + "type": "external", + "external": {"url": image_url}, + } + ] + } + cover = {"type": "external", "external": {"url": image_url}} + icon = {"type": "emoji", "emoji": "🎉"} + parent = { + "type": "database_id", + "database_id": secrets.database_id.get_secret_value(), + } + + return await notion.pages.create( + parent=parent, properties=properties, icon=icon, cover=cover + ) diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..81e0f88 --- /dev/null +++ b/app/views.py @@ -0,0 +1,10 @@ +def start_bot_message(): + return "Бот запущен" + + +def stop_bot_message(): + return "Бот остановлен" + + +def start_text(): + return """Дратути""" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..78e8fb9 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,6 @@ +services: + bot: + build: . + restart: always + volumes: + - .:/code diff --git a/main.py b/main.py new file mode 100644 index 0000000..b0580b6 --- /dev/null +++ b/main.py @@ -0,0 +1,33 @@ +import asyncio + +from aiogram import Dispatcher +from aiogram.filters import Command + +from app.handlers.events import start_bot, stop_bot +from app.handlers.message import parse_message +from app.handlers.simple import start_command +from app.middlewares.admin_middleware import AdminMiddleware +from app.middlewares.album_middleware import MediaGroupMiddleware +from app.settings import bot + + +async def start(): + dp = Dispatcher() + + dp.update.middleware(AdminMiddleware()) + dp.message.middleware(MediaGroupMiddleware()) + + dp.startup.register(start_bot) + dp.shutdown.register(stop_bot) + + dp.message.register(start_command, Command(commands="start")) + dp.message.register(parse_message) + + try: + await dp.start_polling(bot) + finally: + await bot.session.close() + + +if __name__ == "__main__": + asyncio.run(start()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..59355fa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,35 @@ +aiofiles==23.2.1 +aiogram==3.5.0 +aiohttp==3.9.5 +aiosignal==1.3.1 +annotated-types==0.6.0 +anyio==4.3.0 +attrs==23.2.0 +black==24.4.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +fleep==1.0.1 +frozenlist==1.4.1 +h11==0.14.0 +httpcore==1.0.5 +httpx==0.27.0 +idna==3.7 +imgur-python==0.2.4 +magic-filter==1.0.12 +multidict==6.0.5 +mypy-extensions==1.0.0 +notion-client==2.2.1 +packaging==24.0 +pathspec==0.12.1 +platformdirs==4.2.1 +pydantic==2.7.1 +pydantic-settings==2.2.1 +pydantic_core==2.18.2 +python-dotenv==1.0.1 +requests==2.31.0 +sniffio==1.3.1 +typing_extensions==4.11.0 +urllib3==2.2.1 +yarl==1.9.4