initial commit

This commit is contained in:
Ivan Ashikhmin 2024-04-27 21:42:08 +04:00
commit ecfd86bead
19 changed files with 294 additions and 0 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
token=
admin_id=
notion_token=
imgur_client_id=
database_id=

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
venv
.idea

10
Dockerfile Normal file
View File

@ -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" ]

12
README.md Normal file
View File

@ -0,0 +1,12 @@
## Notion Bot
Telegram-бот для пересылки сообщений в базу-данных Notion.
## Особенности
- Пересылка сообщения в Notion
- Получение ссылок из сообщения для занесения в URL-столбцы
- Получение изображения из сообщения для сохранения в Notion. Для отправки сообщения используется хостинг изображений Imgur.
## Запуск
Необходимо переименовать `.env.example` в `.env` и прописать соответствующие данные.
Для запуска достаточно выполнить команду `docker compose up -d`.

0
app/__init__.py Normal file
View File

0
app/handlers/__init__.py Normal file
View File

10
app/handlers/events.py Normal file
View File

@ -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())

9
app/handlers/message.py Normal file
View File

@ -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="👌")])

6
app/handlers/simple.py Normal file
View File

@ -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())

View File

View File

@ -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)

View File

@ -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)

25
app/settings.py Normal file
View File

@ -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())

0
app/utils/__init__.py Normal file
View File

View File

@ -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
)

10
app/views.py Normal file
View File

@ -0,0 +1,10 @@
def start_bot_message():
return "Бот запущен"
def stop_bot_message():
return "Бот остановлен"
def start_text():
return """Дратути"""

6
docker-compose.yaml Normal file
View File

@ -0,0 +1,6 @@
services:
bot:
build: .
restart: always
volumes:
- .:/code

33
main.py Normal file
View File

@ -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())

35
requirements.txt Normal file
View File

@ -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