Add initial bot implementation and project configuration files

This commit is contained in:
2026-05-23 22:27:37 +02:00
commit 6b2d2fead2
10 changed files with 235 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
media/
*.session*
.venv

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/kurigram-message-worker.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.14 (kurigram-message-worker)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14 (kurigram-message-worker)" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/kurigram-message-worker.iml" filepath="$PROJECT_DIR$/.idea/kurigram-message-worker.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

121
input_processor.py Normal file
View File

@@ -0,0 +1,121 @@
import abc
import asyncio
import pathlib
import re
import time
from pathlib import Path
from time import sleep
from pyrogram.types import Message, Photo
class Argument(abc.ABC):
def __init__(self, prompt: str, error_prompt: str | None):
self.prompt = prompt
self.error_prompt = error_prompt if error_prompt is not None else prompt
@abc.abstractmethod
async def verify(self, message: Message) -> object | None:
"""
raises ValueError if argument is incorrect
:param message:
:return: None if correct, but stage is the same, otherwise resulting object
"""
pass
class TextArgument(Argument):
def __init__(self, prompt: str, error_prompt: str | None = None, only_text: bool = True):
super().__init__(prompt, error_prompt)
async def verify(self, message: Message) -> str:
if text := message.text:
return text
raise ValueError("No text provided")
class NumberArgument(Argument):
def __init__(self, prompt: str, error_prompt: str | None = None):
super().__init__(prompt, error_prompt)
async def verify(self, message: Message) -> float:
if text := message.text:
if text.isdigit():
return float(text)
raise ValueError("No text provided")
class ImageArgument(Argument):
def __init__(self, prompt: str, error_prompt: str | None = None, require_multiple_images: bool = False, only_image: bool = False):
super().__init__(prompt, error_prompt)
self.require_multiple_images = require_multiple_images
self.only_image = only_image
self.media_group_id = 0
self.media: list[Photo | None] = []
self.locked_by = 0
async def verify(self, message: Message) -> Photo | list[Photo] | None:
# реализую ебучий synchronized из джавы тупорылыми методами, опираясь на асинхронность, а не на многопоточность
lock = time.time_ns()
if self.locked_by == 0:
self.locked_by = lock
if not message.media:
raise ValueError("No image provided")
if not message.photo:
if self.require_multiple_images:
self.media.append(None)
return None
raise ValueError("Not an image")
if message.media_group_id:
if not self.require_multiple_images:
await asyncio.sleep(1)
if self.locked_by == lock:
raise ValueError("Multiple images provided")
return None
self.media_group_id = message.media_group_id
self.media.append(message.photo)
await asyncio.sleep(1) # ждем короче когда другие сообщения пользователя из серии обработаются
if self.locked_by != lock:
return None
if None in self.media:
raise ValueError("Not an image was provided") #похуй на обработку кейса, когда не все медиа - фото.
return self.media #ну и пошел он в пзд со своим линтером
return message.photo.file_id
class Flow:
def __init__(self, flow: list[Argument]):
self.flow = flow
class InputProcessor:
def __init__(self, flow: list[Argument], exit_regex: list[re.Pattern] | None = None):
if len(flow) == 0:
raise ValueError("Flow cannot be empty")
self.flow = flow
self.exit_regex = exit_regex if exit_regex is not None else []
self.stage = 0
self.results = []
async def process_message(self, message: Message) -> list | str | None:
flow = self.flow[self.stage]
try:
if (r := await flow.verify(message)) is not None:
self.results.append(r)
self.stage += 1
if self.stage == len(self.flow):
res = self.results
self.results = None
return res
else:
return None
except ValueError as e:
print(e)
return flow.error_prompt
return self.flow[self.stage].prompt

63
main.py Normal file
View File

@@ -0,0 +1,63 @@
import asyncio
from pathlib import Path
from pyrogram import Client, idle
from pyrogram.types import Message, Photo, InputMediaPhoto
from input_processor import InputProcessor, TextArgument, ImageArgument
api_id = 21279502
api_hash = "d1def8a57d18b3c1ca0b0b8db045d787"
bot_token = "8639597175:AAGNd-afpy2VLrxoTjX7TMsDftgFHrxSW7w"
media_folder = Path(__file__).parent / "media"
if not media_folder.exists():
media_folder.mkdir()
class WithId:
def __init__(self, id):
self.id = id
async def main():
client = Client("bot", api_id, api_hash, bot_token=bot_token)
processors: dict[int, InputProcessor] = {}
@client.on_message()
async def handler(_client: Client, message: Message):
print(message.id)
try:
if user := message.from_user:
if message.text and message.text.startswith("/start"):
processors[user.id] = InputProcessor([
TextArgument("Как тебя зовут?"),
ImageArgument("Отправь фото (≥ 2)", require_multiple_images=True)
])
await message.reply_text(processors[user.id].flow[0].prompt)
return
if proc := processors.get(user.id):
res = await proc.process_message(message)
if res is None:
pass
elif isinstance(res, str):
await message.reply_text(res)
elif isinstance(res, list):
media: list[Photo] = res[1]
file_names = []
for i in media:
file_names.append(media_folder / f"{user.id}_{i.file_id}.jpg")
await _client.download_media(message=i, file_name=str(file_names[-1]))
await message.reply_text("Downloaded!")
await _client.send_media_group(chat_id=int(str(message.chat.id)), media=[InputMediaPhoto(x) for x in file_names])
processors.pop(user.id)
finally:
print(message.id)
await client.start()
print("started")
await idle()
asyncio.run(main())

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
kurigram
TGCrypto