initial commit
This commit is contained in:
commit
ecfd86bead
5
.env.example
Normal file
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
token=
|
||||||
|
admin_id=
|
||||||
|
notion_token=
|
||||||
|
imgur_client_id=
|
||||||
|
database_id=
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
venv
|
||||||
|
.idea
|
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
12
README.md
Normal 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
0
app/__init__.py
Normal file
0
app/handlers/__init__.py
Normal file
0
app/handlers/__init__.py
Normal file
10
app/handlers/events.py
Normal file
10
app/handlers/events.py
Normal 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
9
app/handlers/message.py
Normal 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
6
app/handlers/simple.py
Normal 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())
|
0
app/middlewares/__init__.py
Normal file
0
app/middlewares/__init__.py
Normal file
18
app/middlewares/admin_middleware.py
Normal file
18
app/middlewares/admin_middleware.py
Normal 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)
|
33
app/middlewares/album_middleware.py
Normal file
33
app/middlewares/album_middleware.py
Normal 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
25
app/settings.py
Normal 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
0
app/utils/__init__.py
Normal file
79
app/utils/send_to_notion.py
Normal file
79
app/utils/send_to_notion.py
Normal 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
10
app/views.py
Normal 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
6
docker-compose.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
33
main.py
Normal file
33
main.py
Normal 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
35
requirements.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user