Add initial bot implementation and project configuration files
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
media/
|
||||
*.session*
|
||||
.venv
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal 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
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
10
.idea/kurigram-message-worker.iml
generated
Normal 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
6
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
121
input_processor.py
Normal 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
63
main.py
Normal 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
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
kurigram
|
||||
TGCrypto
|
||||
Reference in New Issue
Block a user