feat: sending and receiving messages with files, editing text messages
This commit is contained in:
@@ -19,9 +19,11 @@ S3_API_KEY = os.environ.get('S3_API_KEY')
|
|||||||
|
|
||||||
BILLING_API_KEY = os.environ.get('BILLING_API_KEY')
|
BILLING_API_KEY = os.environ.get('BILLING_API_KEY')
|
||||||
|
|
||||||
|
CHAT_TELEGRAM_BOT_TOKEN = os.environ.get('CHAT_TELEGRAM_BOT_TOKEN')
|
||||||
CHAT_CONNECTOR_API_KEY = os.environ.get('CHAT_CONNECTOR_API_KEY')
|
CHAT_CONNECTOR_API_KEY = os.environ.get('CHAT_CONNECTOR_API_KEY')
|
||||||
CHATS_SYNC_URL = os.environ.get('CHATS_SYNC_URL')
|
CHATS_SYNC_URL = os.environ.get('CHATS_SYNC_URL')
|
||||||
|
|
||||||
|
# Kafka
|
||||||
KAFKA_URL = os.environ.get('KAFKA_URL')
|
KAFKA_URL = os.environ.get('KAFKA_URL')
|
||||||
KAFKA_CONSUMER_TOPIC = os.environ.get('KAFKA_CONSUMER_TOPIC')
|
KAFKA_CONSUMER_TOPIC = os.environ.get('KAFKA_CONSUMER_TOPIC')
|
||||||
KAFKA_PRODUCER_TOPIC = os.environ.get('KAFKA_PRODUCER_TOPIC')
|
KAFKA_PRODUCER_TOPIC = os.environ.get('KAFKA_PRODUCER_TOPIC')
|
||||||
|
|||||||
21
external/chat/chat_client.py
vendored
21
external/chat/chat_client.py
vendored
@@ -1,5 +1,6 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import jwt
|
import jwt
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
from backend.config import CHATS_SYNC_URL, CHAT_CONNECTOR_API_KEY
|
from backend.config import CHATS_SYNC_URL, CHAT_CONNECTOR_API_KEY
|
||||||
from external.chat.schemas import *
|
from external.chat.schemas import *
|
||||||
@@ -22,7 +23,6 @@ class ChatClient:
|
|||||||
async def _method(self, http_method, method, **kwargs):
|
async def _method(self, http_method, method, **kwargs):
|
||||||
async with aiohttp.ClientSession(headers=self.headers) as session:
|
async with aiohttp.ClientSession(headers=self.headers) as session:
|
||||||
async with session.request(http_method, self.base_url + method, **kwargs) as response:
|
async with session.request(http_method, self.base_url + method, **kwargs) as response:
|
||||||
print(response)
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
||||||
async def create_group(self, request: ExternalCreateGroupRequest) -> ExternalCreateGroupResponse:
|
async def create_group(self, request: ExternalCreateGroupRequest) -> ExternalCreateGroupResponse:
|
||||||
@@ -34,3 +34,22 @@ class ChatClient:
|
|||||||
json_data = request.model_dump()
|
json_data = request.model_dump()
|
||||||
response = await self._method('POST', self.groups_endpoint + '/topic/create', json=json_data)
|
response = await self._method('POST', self.groups_endpoint + '/topic/create', json=json_data)
|
||||||
return ExternalCreateTopicResponse.model_validate(response)
|
return ExternalCreateTopicResponse.model_validate(response)
|
||||||
|
|
||||||
|
async def send_messages_with_files(
|
||||||
|
self,
|
||||||
|
tg_group_id: str,
|
||||||
|
tg_topic_id: int,
|
||||||
|
caption: str,
|
||||||
|
files: list[UploadFile],
|
||||||
|
) -> ExternalSendMessagesWithFilesResponse:
|
||||||
|
query_params = f'?tg_group_id={tg_group_id}&tg_topic_id={tg_topic_id}&caption={caption}'
|
||||||
|
|
||||||
|
data = aiohttp.FormData(default_to_multipart=True)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
content = await file.read()
|
||||||
|
data.add_field('files', content, filename=file.filename, content_type=file.content_type)
|
||||||
|
|
||||||
|
response = await self._method('POST', self.chats_sync_endpoint + '/send' + query_params, data=data)
|
||||||
|
|
||||||
|
return ExternalSendMessagesWithFilesResponse.model_validate(response)
|
||||||
|
|||||||
22
external/chat/schemas.py
vendored
22
external/chat/schemas.py
vendored
@@ -1,9 +1,25 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from schemas.base import BaseSchema
|
from schemas.base import BaseSchema, OkMessageSchema
|
||||||
|
|
||||||
|
|
||||||
|
# region Entities
|
||||||
|
|
||||||
|
class ExternalSendFileSchema(BaseSchema):
|
||||||
|
buffer: bytes
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalMessageFileSchema(BaseSchema):
|
||||||
|
file_path: str
|
||||||
|
type: str
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region Requests
|
# region Requests
|
||||||
|
|
||||||
class ExternalCreateGroupRequest(BaseSchema):
|
class ExternalCreateGroupRequest(BaseSchema):
|
||||||
@@ -29,4 +45,8 @@ class ExternalCreateGroupResponse(BaseSchema):
|
|||||||
class ExternalCreateTopicResponse(BaseSchema):
|
class ExternalCreateTopicResponse(BaseSchema):
|
||||||
tg_topic_id: int
|
tg_topic_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalSendMessagesWithFilesResponse(OkMessageSchema):
|
||||||
|
files: list[ExternalMessageFileSchema]
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
1
external/kafka/consumer.py
vendored
1
external/kafka/consumer.py
vendored
@@ -24,6 +24,7 @@ async def consume_messages():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async for message in consumer:
|
async for message in consumer:
|
||||||
|
print("consume")
|
||||||
await consumer_service.consume_message(message)
|
await consumer_service.consume_message(message)
|
||||||
finally:
|
finally:
|
||||||
await consumer.stop()
|
await consumer.stop()
|
||||||
|
|||||||
17
external/kafka/schemas/consumer.py
vendored
17
external/kafka/schemas/consumer.py
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from schemas.base import OkMessageSchema, BaseSchema
|
from schemas.base import OkMessageSchema, BaseSchema
|
||||||
|
|
||||||
|
|
||||||
@@ -10,11 +12,19 @@ class TelegramUserSchema(BaseSchema):
|
|||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class MessageFileSchema(BaseSchema):
|
||||||
|
file_path: str
|
||||||
|
type: str
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
|
||||||
|
|
||||||
class MessageFromTelegramSchema(BaseSchema):
|
class MessageFromTelegramSchema(BaseSchema):
|
||||||
group_id: str
|
group_id: str
|
||||||
tg_topic_id: int
|
tg_topic_id: int
|
||||||
text: str
|
text: Optional[str]
|
||||||
sender: TelegramUserSchema
|
sender: TelegramUserSchema
|
||||||
|
file: Optional[MessageFileSchema]
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
@@ -41,4 +51,9 @@ class SendMessageToConnectorResponse(BaseConnectorResponse):
|
|||||||
class DeleteMessageResponse(BaseConnectorResponse):
|
class DeleteMessageResponse(BaseConnectorResponse):
|
||||||
message_id: int
|
message_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class EditMessageResponse(BaseConnectorResponse):
|
||||||
|
message_id: int
|
||||||
|
text: str
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
9
external/kafka/schemas/producer.py
vendored
9
external/kafka/schemas/producer.py
vendored
@@ -11,8 +11,11 @@ class BaseMessageSchema(BaseSchema):
|
|||||||
group_id: str
|
group_id: str
|
||||||
|
|
||||||
|
|
||||||
class MessageSchema(BaseMessageSchema):
|
class EditMessageSchema(BaseMessageSchema):
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
class MessageSchema(EditMessageSchema):
|
||||||
topic_id: int
|
topic_id: int
|
||||||
|
|
||||||
|
|
||||||
@@ -33,4 +36,8 @@ class SendMessageToConnectorRequest(BaseConnectorRequest):
|
|||||||
class SendMessageDeletingToConnectorRequest(BaseConnectorRequest):
|
class SendMessageDeletingToConnectorRequest(BaseConnectorRequest):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SendMessageEditingToConnectorRequest(BaseConnectorRequest):
|
||||||
|
message: EditMessageSchema
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
24
external/kafka/services/consumer_service.py
vendored
24
external/kafka/services/consumer_service.py
vendored
@@ -1,6 +1,5 @@
|
|||||||
import pickle
|
import pickle
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from aiokafka import ConsumerRecord
|
from aiokafka import ConsumerRecord
|
||||||
@@ -8,13 +7,14 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from external.kafka.enums import KafkaMessageType
|
from external.kafka.enums import KafkaMessageType
|
||||||
from external.kafka.schemas.consumer import *
|
from external.kafka.schemas.consumer import *
|
||||||
from models import Message, MessageStatus, TgUser, Chat, TgGroup
|
from models import Message, MessageStatus, TgUser, Chat, TgGroup, MessageFile
|
||||||
from services.base import BaseService
|
from services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
class ConsumerService(BaseService):
|
class ConsumerService(BaseService):
|
||||||
async def consume_message(self, message: ConsumerRecord):
|
async def consume_message(self, message: ConsumerRecord):
|
||||||
value = pickle.loads(message.value)
|
value = pickle.loads(message.value)
|
||||||
|
print("Consumer: received message: ", value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if 'ok' in value:
|
if 'ok' in value:
|
||||||
@@ -36,7 +36,8 @@ class ConsumerService(BaseService):
|
|||||||
response = SendMessageToConnectorResponse.model_validate(value)
|
response = SendMessageToConnectorResponse.model_validate(value)
|
||||||
await self._process_connector_send_response(response)
|
await self._process_connector_send_response(response)
|
||||||
case KafkaMessageType.EDIT:
|
case KafkaMessageType.EDIT:
|
||||||
pass
|
response = EditMessageResponse.model_validate(value)
|
||||||
|
await self._process_connector_edit_response(response)
|
||||||
case KafkaMessageType.DELETE:
|
case KafkaMessageType.DELETE:
|
||||||
response = DeleteMessageResponse.model_validate(value)
|
response = DeleteMessageResponse.model_validate(value)
|
||||||
await self._process_connector_delete_response(response)
|
await self._process_connector_delete_response(response)
|
||||||
@@ -59,6 +60,16 @@ class ConsumerService(BaseService):
|
|||||||
|
|
||||||
message = await self.session.get(Message, response.message_id)
|
message = await self.session.get(Message, response.message_id)
|
||||||
message.is_deleted = True
|
message.is_deleted = True
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
async def _process_connector_edit_response(self, response: EditMessageResponse):
|
||||||
|
if not response.ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = await self.session.get(Message, response.message_id)
|
||||||
|
message.text = response.text
|
||||||
|
message.is_edited = True
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
async def _get_chat(self, group_id: str, tg_topic_id: int) -> Optional[Chat]:
|
async def _get_chat(self, group_id: str, tg_topic_id: int) -> Optional[Chat]:
|
||||||
stmt = (
|
stmt = (
|
||||||
@@ -81,12 +92,17 @@ class ConsumerService(BaseService):
|
|||||||
if not chat:
|
if not chat:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
file = None
|
||||||
|
if request.message.file:
|
||||||
|
file = MessageFile(**request.message.file.model_dump())
|
||||||
|
|
||||||
message = Message(
|
message = Message(
|
||||||
text=request.message.text,
|
text=request.message.text if request.message.text else "",
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
tg_sender_id=tg_sender.id,
|
tg_sender_id=tg_sender.id,
|
||||||
chat_id=chat.id,
|
chat_id=chat.id,
|
||||||
status=MessageStatus.success,
|
status=MessageStatus.success,
|
||||||
|
file=file,
|
||||||
)
|
)
|
||||||
self.session.add(message)
|
self.session.add(message)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|||||||
93
external/kafka/services/producer_service.py
vendored
93
external/kafka/services/producer_service.py
vendored
@@ -6,12 +6,21 @@ from aiohttp import ClientConnectorError
|
|||||||
from backend.config import KAFKA_PRODUCER_TOPIC, CHAT_CONNECTOR_API_KEY
|
from backend.config import KAFKA_PRODUCER_TOPIC, CHAT_CONNECTOR_API_KEY
|
||||||
from external.kafka import producer
|
from external.kafka import producer
|
||||||
from external.kafka.enums import KafkaMessageType
|
from external.kafka.enums import KafkaMessageType
|
||||||
from external.kafka.schemas.producer import MessageSchema, SendMessageToConnectorRequest, \
|
from external.kafka.schemas.producer import *
|
||||||
SendMessageDeletingToConnectorRequest, BaseMessageSchema
|
|
||||||
from services.base import BaseService
|
from services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
class ProducerService(BaseService):
|
class ProducerService(BaseService):
|
||||||
|
@staticmethod
|
||||||
|
async def _send_message(request: BaseConnectorRequest):
|
||||||
|
try:
|
||||||
|
await producer.send(KAFKA_PRODUCER_TOPIC, value=pickle.dumps(request.model_dump()))
|
||||||
|
except ClientConnectorError:
|
||||||
|
return False, 'Ошибка подключения к коннектору'
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
return True, 'Сообщение отправлено'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def send_message_to_connector(
|
async def send_message_to_connector(
|
||||||
message_text: str,
|
message_text: str,
|
||||||
@@ -19,42 +28,50 @@ class ProducerService(BaseService):
|
|||||||
topic_id: int,
|
topic_id: int,
|
||||||
message_id: int,
|
message_id: int,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
try:
|
request = SendMessageToConnectorRequest(
|
||||||
request = SendMessageToConnectorRequest(
|
message_type=KafkaMessageType.SEND,
|
||||||
message=MessageSchema(
|
app_auth_key=CHAT_CONNECTOR_API_KEY,
|
||||||
message_id=message_id,
|
message=MessageSchema(
|
||||||
text=message_text,
|
message_id=message_id,
|
||||||
group_id=str(group_id),
|
text=message_text,
|
||||||
topic_id=topic_id,
|
group_id=str(group_id),
|
||||||
),
|
topic_id=topic_id,
|
||||||
message_type=KafkaMessageType.SEND,
|
),
|
||||||
app_auth_key=CHAT_CONNECTOR_API_KEY,
|
)
|
||||||
)
|
return await ProducerService._send_message(request)
|
||||||
await producer.send(KAFKA_PRODUCER_TOPIC, value=pickle.dumps(request.model_dump()))
|
|
||||||
except ClientConnectorError:
|
|
||||||
return False, 'Ошибка подключения к коннектору'
|
|
||||||
except Exception as e:
|
|
||||||
return False, str(e)
|
|
||||||
|
|
||||||
return True, 'Сообщение отправлено'
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def send_message_deleting_to_connector(message_id: int, tg_message_id: int, group_id: UUID) -> tuple[bool, str]:
|
async def send_message_deleting_to_connector(
|
||||||
try:
|
message_id: int,
|
||||||
request = SendMessageDeletingToConnectorRequest(
|
tg_message_id: int,
|
||||||
message_type=KafkaMessageType.DELETE,
|
group_id: UUID,
|
||||||
app_auth_key=CHAT_CONNECTOR_API_KEY,
|
) -> tuple[bool, str]:
|
||||||
message=BaseMessageSchema(
|
request = SendMessageDeletingToConnectorRequest(
|
||||||
message_id=message_id,
|
message_type=KafkaMessageType.DELETE,
|
||||||
tg_message_id=tg_message_id,
|
app_auth_key=CHAT_CONNECTOR_API_KEY,
|
||||||
group_id=str(group_id),
|
message=BaseMessageSchema(
|
||||||
),
|
message_id=message_id,
|
||||||
|
tg_message_id=tg_message_id,
|
||||||
|
group_id=str(group_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return await ProducerService._send_message(request)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def send_message_editing_to_connector(
|
||||||
|
message_id: int,
|
||||||
|
tg_message_id: int,
|
||||||
|
group_id: UUID,
|
||||||
|
text: str,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
request = SendMessageEditingToConnectorRequest(
|
||||||
|
message_type=KafkaMessageType.EDIT,
|
||||||
|
app_auth_key=CHAT_CONNECTOR_API_KEY,
|
||||||
|
message=EditMessageSchema(
|
||||||
|
message_id=message_id,
|
||||||
|
tg_message_id=tg_message_id,
|
||||||
|
group_id=str(group_id),
|
||||||
|
text=text,
|
||||||
)
|
)
|
||||||
await producer.send(KAFKA_PRODUCER_TOPIC, value=pickle.dumps(request.model_dump()))
|
)
|
||||||
except ClientConnectorError:
|
return await ProducerService._send_message(request)
|
||||||
return False, 'Ошибка подключения к коннектору'
|
|
||||||
except Exception as e:
|
|
||||||
return False, str(e)
|
|
||||||
|
|
||||||
return True, 'Сообщение отправлено'
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,23 @@ class Chat(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageFile(BaseModel):
|
||||||
|
__tablename__ = 'message_files'
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
file_path: Mapped[str] = mapped_column(nullable=False)
|
||||||
|
type: Mapped[Optional[str]] = mapped_column(nullable=True)
|
||||||
|
file_name: Mapped[str] = mapped_column(nullable=False)
|
||||||
|
file_size: Mapped[int] = mapped_column(BigInteger(), nullable=True, comment='Размер файла в байтах')
|
||||||
|
|
||||||
|
message_id: Mapped[int] = mapped_column(ForeignKey('messages.id'))
|
||||||
|
message: Mapped['Message'] = relationship(
|
||||||
|
'Message',
|
||||||
|
lazy='noload',
|
||||||
|
back_populates='file',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MessageStatus(enum.Enum):
|
class MessageStatus(enum.Enum):
|
||||||
sending = 'SENDING'
|
sending = 'SENDING'
|
||||||
success = 'SUCCESS'
|
success = 'SUCCESS'
|
||||||
@@ -125,6 +142,7 @@ class Message(BaseModel):
|
|||||||
created_at: Mapped[datetime] = mapped_column(nullable=False)
|
created_at: Mapped[datetime] = mapped_column(nullable=False)
|
||||||
status: Mapped[MessageStatus] = mapped_column(Enum(MessageStatus), nullable=False)
|
status: Mapped[MessageStatus] = mapped_column(Enum(MessageStatus), nullable=False)
|
||||||
is_deleted: Mapped[bool] = mapped_column(default=False, server_default='0', nullable=False)
|
is_deleted: Mapped[bool] = mapped_column(default=False, server_default='0', nullable=False)
|
||||||
|
is_edited: Mapped[bool] = mapped_column(default=False, server_default='0', nullable=False)
|
||||||
|
|
||||||
tg_sender_id: Mapped[Optional[int]] = mapped_column(
|
tg_sender_id: Mapped[Optional[int]] = mapped_column(
|
||||||
ForeignKey('tg_users.id'),
|
ForeignKey('tg_users.id'),
|
||||||
@@ -152,3 +170,9 @@ class Message(BaseModel):
|
|||||||
lazy='noload',
|
lazy='noload',
|
||||||
back_populates='messages',
|
back_populates='messages',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
file: Mapped[Optional[MessageFile]] = relationship(
|
||||||
|
'MessageFile',
|
||||||
|
back_populates='message',
|
||||||
|
lazy='selectin',
|
||||||
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ aiokafka
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
aiohttp
|
aiohttp
|
||||||
aiohttp[speedups]
|
aiohttp[speedups]
|
||||||
|
requests
|
||||||
openpyxl==3.0.10
|
openpyxl==3.0.10
|
||||||
lexorank-py
|
lexorank-py
|
||||||
celery[redis]
|
celery[redis]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, UploadFile
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.session import get_session
|
from backend.session import get_session
|
||||||
@@ -15,16 +15,42 @@ chat_router = APIRouter(
|
|||||||
|
|
||||||
@chat_router.post(
|
@chat_router.post(
|
||||||
'/message',
|
'/message',
|
||||||
operation_id='send_message',
|
operation_id='send_text_message',
|
||||||
response_model=SendMessageResponse,
|
response_model=SendTextMessageResponse,
|
||||||
)
|
)
|
||||||
async def send_message(
|
async def send_text_message(
|
||||||
session: Annotated[AsyncSession, Depends(get_session)],
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
request: SendMessageRequest,
|
request: SendTextMessageRequest,
|
||||||
):
|
):
|
||||||
return await ChatService(session).send_message(request)
|
return await ChatService(session).send_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@chat_router.post(
|
||||||
|
'/message/repeat',
|
||||||
|
operation_id='repeat_sending_text_message',
|
||||||
|
response_model=RepeatSendingTextMessageResponse,
|
||||||
|
)
|
||||||
|
async def repeat_sending_text_message(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
request: RepeatSendingTextMessageRequest,
|
||||||
|
):
|
||||||
|
return await ChatService(session).repeat_sending_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@chat_router.post(
|
||||||
|
'/message/files',
|
||||||
|
operation_id='send_messages_with_files',
|
||||||
|
response_model=LoadMessagesResponse,
|
||||||
|
)
|
||||||
|
async def send_messages_with_files(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
files: list[UploadFile],
|
||||||
|
chat_id: int,
|
||||||
|
caption: str,
|
||||||
|
):
|
||||||
|
return await ChatService(session).send_messages_with_files(files, chat_id, caption)
|
||||||
|
|
||||||
|
|
||||||
@chat_router.delete(
|
@chat_router.delete(
|
||||||
'/message/{message_id}',
|
'/message/{message_id}',
|
||||||
operation_id='delete_message',
|
operation_id='delete_message',
|
||||||
@@ -37,6 +63,18 @@ async def delete_message(
|
|||||||
return await ChatService(session).delete_message(message_id)
|
return await ChatService(session).delete_message(message_id)
|
||||||
|
|
||||||
|
|
||||||
|
@chat_router.patch(
|
||||||
|
'/message',
|
||||||
|
operation_id='edit_message',
|
||||||
|
response_model=EditMessageResponse,
|
||||||
|
)
|
||||||
|
async def edit_message(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
request: EditMessageRequest,
|
||||||
|
):
|
||||||
|
return await ChatService(session).edit_message(request)
|
||||||
|
|
||||||
|
|
||||||
@chat_router.post(
|
@chat_router.post(
|
||||||
'/',
|
'/',
|
||||||
operation_id='get_chat',
|
operation_id='get_chat',
|
||||||
@@ -71,3 +109,13 @@ async def get_messages(
|
|||||||
request: GetMessagesRequest,
|
request: GetMessagesRequest,
|
||||||
):
|
):
|
||||||
return await ChatService(session).get_messages(request)
|
return await ChatService(session).get_messages(request)
|
||||||
|
|
||||||
|
|
||||||
|
@chat_router.get(
|
||||||
|
'/tg-file/{file_id}',
|
||||||
|
)
|
||||||
|
async def get_tg_file(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
file_id: int,
|
||||||
|
):
|
||||||
|
return await ChatService(session).get_tg_file(file_id)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
from schemas.base import BaseSchema, OkMessageSchema
|
from schemas.base import BaseSchema, OkMessageSchema
|
||||||
|
|
||||||
@@ -13,10 +13,12 @@ class TgUserSchema(BaseSchema):
|
|||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
class MessageFromTgSchema(BaseSchema):
|
class MessageFileSchema(BaseSchema):
|
||||||
chat_id: int
|
id: int
|
||||||
text: str
|
file_path: str
|
||||||
sender: TgUserSchema
|
type: str
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
|
||||||
|
|
||||||
class BaseMessageSchema(BaseSchema):
|
class BaseMessageSchema(BaseSchema):
|
||||||
@@ -24,11 +26,21 @@ class BaseMessageSchema(BaseSchema):
|
|||||||
chat_id: int
|
chat_id: int
|
||||||
|
|
||||||
|
|
||||||
class MessageSchema(BaseMessageSchema):
|
class EditMessageSchema(BaseMessageSchema):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class MessageSchema(EditMessageSchema):
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
tg_sender: Optional[TgUserSchema]
|
tg_sender: Optional[TgUserSchema]
|
||||||
status: str
|
status: str
|
||||||
|
is_edited: bool
|
||||||
|
file: Optional[MessageFileSchema] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RepeatSendingMessageSchema(BaseMessageSchema):
|
||||||
|
text: str
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
class TgGroupSchema(BaseSchema):
|
class TgGroupSchema(BaseSchema):
|
||||||
@@ -46,10 +58,18 @@ class ChatSchema(BaseSchema):
|
|||||||
|
|
||||||
# region Requests
|
# region Requests
|
||||||
|
|
||||||
class SendMessageRequest(BaseSchema):
|
class SendTextMessageRequest(BaseSchema):
|
||||||
message: BaseMessageSchema
|
message: BaseMessageSchema
|
||||||
|
|
||||||
|
|
||||||
|
class RepeatSendingTextMessageRequest(BaseSchema):
|
||||||
|
message: RepeatSendingMessageSchema
|
||||||
|
|
||||||
|
|
||||||
|
class EditMessageRequest(BaseSchema):
|
||||||
|
message: EditMessageSchema
|
||||||
|
|
||||||
|
|
||||||
class GetChatRequest(BaseSchema):
|
class GetChatRequest(BaseSchema):
|
||||||
client_id: int
|
client_id: int
|
||||||
card_id: Optional[int]
|
card_id: Optional[int]
|
||||||
@@ -69,7 +89,15 @@ class GetMessagesRequest(BaseSchema):
|
|||||||
|
|
||||||
# region Responses
|
# region Responses
|
||||||
|
|
||||||
class SendMessageResponse(OkMessageSchema):
|
class SendTextMessageResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RepeatSendingTextMessageResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LoadMessagesResponse(OkMessageSchema):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +105,10 @@ class DeleteMessageResponse(OkMessageSchema):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EditMessageResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GetChatResponse(BaseSchema):
|
class GetChatResponse(BaseSchema):
|
||||||
chat: Optional[ChatSchema]
|
chat: Optional[ChatSchema]
|
||||||
|
|
||||||
|
|||||||
111
services/chat.py
111
services/chat.py
@@ -1,14 +1,15 @@
|
|||||||
from uuid import UUID
|
import requests
|
||||||
|
|
||||||
from aiohttp import ClientConnectorError
|
from aiohttp import ClientConnectorError
|
||||||
|
from fastapi import HTTPException, UploadFile
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
|
||||||
from backend.config import CHAT_CONNECTOR_API_KEY
|
from backend.config import CHAT_CONNECTOR_API_KEY, CHAT_TELEGRAM_BOT_TOKEN
|
||||||
from external.chat.chat_client import ChatClient
|
from external.chat.chat_client import ChatClient
|
||||||
from external.chat.schemas import ExternalCreateGroupRequest, ExternalCreateTopicRequest
|
from external.chat.schemas import *
|
||||||
from external.kafka.services.producer_service import ProducerService
|
from external.kafka.services.producer_service import ProducerService
|
||||||
from models import Message, Chat, MessageStatus, TgGroup, Client, Card
|
from models import Message, Chat, MessageStatus, TgGroup, Client, Card, MessageFile
|
||||||
from schemas.chat import *
|
from schemas.chat import *
|
||||||
from services.base import BaseService
|
from services.base import BaseService
|
||||||
|
|
||||||
@@ -161,10 +162,10 @@ class ChatService(BaseService):
|
|||||||
messages = (await self.session.scalars(stmt)).all()
|
messages = (await self.session.scalars(stmt)).all()
|
||||||
return GetMessagesResponse(messages=messages)
|
return GetMessagesResponse(messages=messages)
|
||||||
|
|
||||||
async def send_message(self, request: SendMessageRequest) -> SendMessageResponse:
|
async def send_message(self, request: SendTextMessageRequest) -> SendTextMessageResponse:
|
||||||
chat: Optional[Chat] = await self.session.get(Chat, request.message.chat_id)
|
chat: Optional[Chat] = await self.session.get(Chat, request.message.chat_id)
|
||||||
if not chat:
|
if not chat:
|
||||||
return SendMessageResponse(ok=False, message=f'Чат с ID: {request.message.chat_id} не найден')
|
return SendTextMessageResponse(ok=False, message=f'Чат с ID: {request.message.chat_id} не найден')
|
||||||
|
|
||||||
message = Message(
|
message = Message(
|
||||||
text=request.message.text,
|
text=request.message.text,
|
||||||
@@ -182,7 +183,67 @@ class ChatService(BaseService):
|
|||||||
message.id,
|
message.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return SendMessageResponse(ok=ok, message=message)
|
return SendTextMessageResponse(ok=ok, message=message)
|
||||||
|
|
||||||
|
async def repeat_sending_message(
|
||||||
|
self,
|
||||||
|
request: RepeatSendingTextMessageRequest,
|
||||||
|
) -> RepeatSendingTextMessageResponse:
|
||||||
|
message: Optional[Message] = await self._get_message_by_id(request.message.id)
|
||||||
|
if not message:
|
||||||
|
return RepeatSendingTextMessageResponse(ok=False, message=f'Сообщение с ID: {request.message.id} не найдено')
|
||||||
|
|
||||||
|
ok, msg = await ProducerService.send_message_to_connector(
|
||||||
|
request.message.text,
|
||||||
|
message.chat.tg_group_id,
|
||||||
|
message.chat.tg_topic_id,
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
message.status = MessageStatus.sending
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
return RepeatSendingTextMessageResponse(ok=ok, message=msg)
|
||||||
|
|
||||||
|
async def send_messages_with_files(
|
||||||
|
self,
|
||||||
|
files: list[UploadFile],
|
||||||
|
chat_id: int,
|
||||||
|
caption: str,
|
||||||
|
) -> LoadMessagesResponse:
|
||||||
|
chat: Optional[Chat] = await self.session.get(Chat, chat_id)
|
||||||
|
if not chat:
|
||||||
|
return SendTextMessageResponse(ok=False, message=f'Чат с ID: {chat_id} не найден')
|
||||||
|
|
||||||
|
chat_client = ChatClient(api_key=CHAT_CONNECTOR_API_KEY)
|
||||||
|
response = await chat_client.send_messages_with_files(
|
||||||
|
str(chat.tg_group_id),
|
||||||
|
chat.tg_topic_id,
|
||||||
|
caption,
|
||||||
|
files
|
||||||
|
)
|
||||||
|
|
||||||
|
last_message = None
|
||||||
|
for file_schema in response.files:
|
||||||
|
file = MessageFile(**file_schema.model_dump())
|
||||||
|
self.session.add(file)
|
||||||
|
|
||||||
|
message = Message(
|
||||||
|
text='',
|
||||||
|
created_at=datetime.now(),
|
||||||
|
chat_id=chat_id,
|
||||||
|
status=MessageStatus.success,
|
||||||
|
file=file,
|
||||||
|
)
|
||||||
|
last_message = message
|
||||||
|
self.session.add(message)
|
||||||
|
|
||||||
|
if last_message:
|
||||||
|
last_message.text = caption
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
return LoadMessagesResponse(ok=response.ok, message=response.message)
|
||||||
|
|
||||||
async def _get_message_by_id(self, message_id: int) -> Optional[Message]:
|
async def _get_message_by_id(self, message_id: int) -> Optional[Message]:
|
||||||
stmt = (
|
stmt = (
|
||||||
@@ -203,7 +264,35 @@ class ChatService(BaseService):
|
|||||||
message.chat.tg_group_id,
|
message.chat.tg_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
message.is_deleted = True
|
|
||||||
await self.session.commit()
|
|
||||||
|
|
||||||
return DeleteMessageResponse(ok=ok, message=msg)
|
return DeleteMessageResponse(ok=ok, message=msg)
|
||||||
|
|
||||||
|
async def edit_message(self, request: EditMessageRequest) -> EditMessageResponse:
|
||||||
|
message: Optional[Message] = await self._get_message_by_id(request.message.id)
|
||||||
|
if not message:
|
||||||
|
return EditMessageResponse(ok=False, message=f'Сообщение с ID: {request.message.id} не найдено')
|
||||||
|
|
||||||
|
ok, msg = await ProducerService.send_message_editing_to_connector(
|
||||||
|
message.id,
|
||||||
|
message.tg_message_id,
|
||||||
|
message.chat.tg_group_id,
|
||||||
|
request.message.text,
|
||||||
|
)
|
||||||
|
|
||||||
|
return EditMessageResponse(ok=ok, message=msg)
|
||||||
|
|
||||||
|
async def get_tg_file(self, file_id: int) -> StreamingResponse:
|
||||||
|
file: Optional[MessageFile] = await self.session.get(MessageFile, file_id)
|
||||||
|
if not file:
|
||||||
|
raise HTTPException(status_code=404, detail=f'Файл с ID {file_id} не найден')
|
||||||
|
|
||||||
|
url: str = f'https://api.telegram.org/file/bot{CHAT_TELEGRAM_BOT_TOKEN}/{file.file_path}'
|
||||||
|
print(f'URL = {url}')
|
||||||
|
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(status_code=response.status_code, detail="Error fetching file")
|
||||||
|
|
||||||
|
content_type = response.headers.get("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
return StreamingResponse(response.iter_content(chunk_size=8192), media_type=content_type)
|
||||||
|
|||||||
Reference in New Issue
Block a user