Add upload images
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, UploadFile, File, Form
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
from app.api.deps import DbSession, CurrentUser, require_admin_with_2fa
|
||||
from app.models import (
|
||||
@@ -531,9 +532,10 @@ async def get_logs(
|
||||
@limiter.limit("1/minute")
|
||||
async def broadcast_to_all(
|
||||
request: Request,
|
||||
data: BroadcastRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
message: str = Form(""),
|
||||
media: list[UploadFile] = File(default=[]),
|
||||
):
|
||||
"""Send broadcast message to all users with Telegram linked. Admin only."""
|
||||
require_admin_with_2fa(current_user)
|
||||
@@ -547,15 +549,40 @@ async def broadcast_to_all(
|
||||
total_count = len(users)
|
||||
sent_count = 0
|
||||
|
||||
# Read media files if provided (up to 10 files, Telegram limit)
|
||||
media_items = []
|
||||
for file in media[:10]:
|
||||
if file and file.filename:
|
||||
file_data = await file.read()
|
||||
content_type = file.content_type or ""
|
||||
if content_type.startswith("image/"):
|
||||
media_items.append({
|
||||
"type": "photo",
|
||||
"data": file_data,
|
||||
"filename": file.filename,
|
||||
"content_type": content_type
|
||||
})
|
||||
elif content_type.startswith("video/"):
|
||||
media_items.append({
|
||||
"type": "video",
|
||||
"data": file_data,
|
||||
"filename": file.filename,
|
||||
"content_type": content_type
|
||||
})
|
||||
|
||||
for user in users:
|
||||
if await telegram_notifier.send_message(user.telegram_id, data.message):
|
||||
if await telegram_notifier.send_media_message(
|
||||
user.telegram_id,
|
||||
text=message if message.strip() else None,
|
||||
media_items=media_items if media_items else None
|
||||
):
|
||||
sent_count += 1
|
||||
|
||||
# Log action
|
||||
await log_admin_action(
|
||||
db, current_user.id, AdminActionType.BROADCAST_ALL.value,
|
||||
"broadcast", 0,
|
||||
{"message": data.message[:100], "sent": sent_count, "total": total_count},
|
||||
{"message": message[:100], "sent": sent_count, "total": total_count, "media_count": len(media_items)},
|
||||
request.client.host if request.client else None
|
||||
)
|
||||
|
||||
@@ -567,9 +594,10 @@ async def broadcast_to_all(
|
||||
async def broadcast_to_marathon(
|
||||
request: Request,
|
||||
marathon_id: int,
|
||||
data: BroadcastRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
message: str = Form(""),
|
||||
media: list[UploadFile] = File(default=[]),
|
||||
):
|
||||
"""Send broadcast message to marathon participants. Admin only."""
|
||||
require_admin_with_2fa(current_user)
|
||||
@@ -580,7 +608,7 @@ async def broadcast_to_marathon(
|
||||
if not marathon:
|
||||
raise HTTPException(status_code=404, detail="Marathon not found")
|
||||
|
||||
# Get participants count
|
||||
# Get participants with telegram
|
||||
total_result = await db.execute(
|
||||
select(User)
|
||||
.join(Participant, Participant.user_id == User.id)
|
||||
@@ -592,15 +620,41 @@ async def broadcast_to_marathon(
|
||||
users = total_result.scalars().all()
|
||||
total_count = len(users)
|
||||
|
||||
sent_count = await telegram_notifier.notify_marathon_participants(
|
||||
db, marathon_id, data.message
|
||||
)
|
||||
# Read media files if provided (up to 10 files, Telegram limit)
|
||||
media_items = []
|
||||
for file in media[:10]:
|
||||
if file and file.filename:
|
||||
file_data = await file.read()
|
||||
content_type = file.content_type or ""
|
||||
if content_type.startswith("image/"):
|
||||
media_items.append({
|
||||
"type": "photo",
|
||||
"data": file_data,
|
||||
"filename": file.filename,
|
||||
"content_type": content_type
|
||||
})
|
||||
elif content_type.startswith("video/"):
|
||||
media_items.append({
|
||||
"type": "video",
|
||||
"data": file_data,
|
||||
"filename": file.filename,
|
||||
"content_type": content_type
|
||||
})
|
||||
|
||||
sent_count = 0
|
||||
for user in users:
|
||||
if await telegram_notifier.send_media_message(
|
||||
user.telegram_id,
|
||||
text=message if message.strip() else None,
|
||||
media_items=media_items if media_items else None
|
||||
):
|
||||
sent_count += 1
|
||||
|
||||
# Log action
|
||||
await log_admin_action(
|
||||
db, current_user.id, AdminActionType.BROADCAST_MARATHON.value,
|
||||
"marathon", marathon_id,
|
||||
{"title": marathon.title, "message": data.message[:100], "sent": sent_count, "total": total_count},
|
||||
{"title": marathon.title, "message": message[:100], "sent": sent_count, "total": total_count, "media_count": len(media_items)},
|
||||
request.client.host if request.client else None
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +54,209 @@ class TelegramNotifier:
|
||||
logger.error(f"Error sending Telegram message: {e}")
|
||||
return False
|
||||
|
||||
async def send_photo(
|
||||
self,
|
||||
chat_id: int,
|
||||
photo: bytes,
|
||||
caption: str | None = None,
|
||||
parse_mode: str = "HTML",
|
||||
filename: str = "photo.jpg",
|
||||
content_type: str = "image/jpeg"
|
||||
) -> bool:
|
||||
"""Send a photo to a Telegram chat."""
|
||||
if not self.bot_token:
|
||||
logger.warning("Telegram bot token not configured")
|
||||
return False
|
||||
|
||||
try:
|
||||
timeout = httpx.Timeout(connect=30.0, read=60.0, write=120.0, pool=30.0)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
data = {"chat_id": str(chat_id)}
|
||||
if caption:
|
||||
data["caption"] = caption
|
||||
data["parse_mode"] = parse_mode
|
||||
|
||||
files = {"photo": (filename, photo, content_type)}
|
||||
|
||||
response = await client.post(
|
||||
f"{self.api_url}/sendPhoto",
|
||||
data=data,
|
||||
files=files,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to send photo to {chat_id}: {response.status_code} - {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending Telegram photo to {chat_id}: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
async def send_video(
|
||||
self,
|
||||
chat_id: int,
|
||||
video: bytes,
|
||||
caption: str | None = None,
|
||||
parse_mode: str = "HTML",
|
||||
filename: str = "video.mp4",
|
||||
content_type: str = "video/mp4"
|
||||
) -> bool:
|
||||
"""Send a video to a Telegram chat."""
|
||||
if not self.bot_token:
|
||||
logger.warning("Telegram bot token not configured")
|
||||
return False
|
||||
|
||||
try:
|
||||
timeout = httpx.Timeout(connect=30.0, read=120.0, write=300.0, pool=30.0)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
data = {"chat_id": str(chat_id)}
|
||||
if caption:
|
||||
data["caption"] = caption
|
||||
data["parse_mode"] = parse_mode
|
||||
|
||||
files = {"video": (filename, video, content_type)}
|
||||
|
||||
response = await client.post(
|
||||
f"{self.api_url}/sendVideo",
|
||||
data=data,
|
||||
files=files,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to send video to {chat_id}: {response.status_code} - {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending Telegram video to {chat_id}: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
async def send_media_group(
|
||||
self,
|
||||
chat_id: int,
|
||||
media_items: list[dict],
|
||||
caption: str | None = None,
|
||||
parse_mode: str = "HTML"
|
||||
) -> bool:
|
||||
"""
|
||||
Send a media group (multiple photos/videos) to a Telegram chat.
|
||||
|
||||
media_items: list of dicts with keys:
|
||||
- type: "photo" or "video"
|
||||
- data: bytes
|
||||
- filename: str
|
||||
- content_type: str
|
||||
"""
|
||||
if not self.bot_token:
|
||||
logger.warning("Telegram bot token not configured")
|
||||
return False
|
||||
|
||||
if not media_items:
|
||||
return False
|
||||
|
||||
try:
|
||||
import json
|
||||
# Use longer timeouts for file uploads
|
||||
timeout = httpx.Timeout(
|
||||
connect=30.0,
|
||||
read=120.0,
|
||||
write=300.0, # 5 minutes for uploading files
|
||||
pool=30.0
|
||||
)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
# Build media array and files dict
|
||||
media_array = []
|
||||
files_dict = {}
|
||||
|
||||
for i, item in enumerate(media_items):
|
||||
attach_name = f"media{i}"
|
||||
media_obj = {
|
||||
"type": item["type"],
|
||||
"media": f"attach://{attach_name}"
|
||||
}
|
||||
# Only first item gets the caption
|
||||
if i == 0 and caption:
|
||||
media_obj["caption"] = caption
|
||||
media_obj["parse_mode"] = parse_mode
|
||||
|
||||
media_array.append(media_obj)
|
||||
files_dict[attach_name] = (
|
||||
item.get("filename", f"file{i}"),
|
||||
item["data"],
|
||||
item.get("content_type", "application/octet-stream")
|
||||
)
|
||||
|
||||
data = {
|
||||
"chat_id": str(chat_id),
|
||||
"media": json.dumps(media_array)
|
||||
}
|
||||
|
||||
logger.info(f"Sending media group to {chat_id}: {len(media_items)} files")
|
||||
response = await client.post(
|
||||
f"{self.api_url}/sendMediaGroup",
|
||||
data=data,
|
||||
files=files_dict,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Successfully sent media group to {chat_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to send media group to {chat_id}: {response.status_code} - {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending Telegram media group to {chat_id}: {type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
async def send_media_message(
|
||||
self,
|
||||
chat_id: int,
|
||||
text: str | None = None,
|
||||
media_type: str | None = None,
|
||||
media_data: bytes | None = None,
|
||||
media_items: list[dict] | None = None,
|
||||
parse_mode: str = "HTML"
|
||||
) -> bool:
|
||||
"""
|
||||
Send a message with optional media.
|
||||
|
||||
For single media: use media_type and media_data
|
||||
For multiple media: use media_items list with dicts containing:
|
||||
- type: "photo" or "video"
|
||||
- data: bytes
|
||||
- filename: str (optional)
|
||||
- content_type: str (optional)
|
||||
"""
|
||||
# Multiple media - use media group
|
||||
if media_items and len(media_items) > 1:
|
||||
return await self.send_media_group(chat_id, media_items, text, parse_mode)
|
||||
|
||||
# Single media from media_items
|
||||
if media_items and len(media_items) == 1:
|
||||
item = media_items[0]
|
||||
if item["type"] == "photo":
|
||||
return await self.send_photo(
|
||||
chat_id, item["data"], text, parse_mode,
|
||||
item.get("filename", "photo.jpg"),
|
||||
item.get("content_type", "image/jpeg")
|
||||
)
|
||||
elif item["type"] == "video":
|
||||
return await self.send_video(
|
||||
chat_id, item["data"], text, parse_mode,
|
||||
item.get("filename", "video.mp4"),
|
||||
item.get("content_type", "video/mp4")
|
||||
)
|
||||
|
||||
# Legacy single media support
|
||||
if media_data and media_type:
|
||||
if media_type == "photo":
|
||||
return await self.send_photo(chat_id, media_data, text, parse_mode)
|
||||
elif media_type == "video":
|
||||
return await self.send_video(chat_id, media_data, text, parse_mode)
|
||||
|
||||
if text:
|
||||
return await self.send_message(chat_id, text, parse_mode)
|
||||
|
||||
return False
|
||||
|
||||
async def notify_user(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
|
||||
Reference in New Issue
Block a user