11import time
22import re
33import logging
4- from telegram import Update , InlineKeyboardButton , InlineKeyboardMarkup
4+ import html
5+ from telegram import Update , InlineKeyboardButton , InlineKeyboardMarkup , InputMediaPhoto , InputMediaVideo
56from telegram .ext import ContextTypes
67from telegram .constants import ParseMode
78from telegram .error import BadRequest , Forbidden
1718logger = logging .getLogger ("Broadcast" )
1819# 内存缓存:互动按钮冷却
1920click_cooldown = {}
21+ # 内存缓存:媒体组暂存,用于相册广播
22+ media_group_cache = {} # media_group_id -> {"items": [..], "ts": float}
23+ msg_to_media_group = {} # admin msg_id -> media_group_id
24+ MEDIA_CACHE_TTL = 300 # 秒
2025
2126async def handle_broadcast_input (update : Update , context : ContextTypes .DEFAULT_TYPE ):
2227 """终极管理员消息入口:处理广播选择、回复追踪、自动路由"""
2328 if update .effective_chat .id != ADMIN_ID : return
2429
2530 msg_id = update .message .message_id
31+ mgid = update .message .media_group_id
32+
33+ # 0. 如为相册,先缓存媒体,保持多图合并
34+ if mgid :
35+ media_group_cache .setdefault (mgid , {"items" : [], "ts" : time .time ()})
36+ entry = media_group_cache [mgid ]
37+ entry ["ts" ] = time .time ()
38+ entry ["items" ].append (_extract_media (update .message ))
39+ msg_to_media_group [msg_id ] = mgid
40+ _cleanup_media_cache ()
2641
2742 # 1. 检查是否是回复消息 (精准追踪)
2843 reply_to = update .message .reply_to_message
@@ -59,6 +74,28 @@ async def handle_broadcast_input(update: Update, context: ContextTypes.DEFAULT_T
5974
6075 await update .message .reply_text (text , reply_markup = InlineKeyboardMarkup (keyboard ), parse_mode = ParseMode .MARKDOWN )
6176
77+ def _extract_media (message ):
78+ """提取相册媒体所需信息"""
79+ base = {
80+ "caption" : message .caption or "" ,
81+ }
82+ if message .photo :
83+ fid = message .photo [- 1 ].file_id
84+ return {** base , "type" : "photo" , "file_id" : fid }
85+ if message .video :
86+ return {** base , "type" : "video" , "file_id" : message .video .file_id }
87+ return None
88+
89+ def _cleanup_media_cache ():
90+ now = time .time ()
91+ expired = [k for k , v in media_group_cache .items () if now - v ["ts" ] > MEDIA_CACHE_TTL ]
92+ for k in expired :
93+ media_group_cache .pop (k , None )
94+ # 同步清理 msg_to_media_group
95+ for mid , mgid in list (msg_to_media_group .items ()):
96+ if mgid not in media_group_cache :
97+ msg_to_media_group .pop (mid , None )
98+
6299async def execute_reply_broadcast (update : Update , context : ContextTypes .DEFAULT_TYPE , targets : dict ):
63100 """精准回复:根据历史映射,去目标群 Reply 对应的那条消息"""
64101 success = 0
@@ -106,7 +143,7 @@ async def callback_broadcast_exec(update: Update, context: ContextTypes.DEFAULT_
106143 "signal" : ("sub_signal" , "topic_signal" , "信号" , True ),
107144 "general" : (None , None , "公共频道" , False )
108145 }
109-
146+
110147 conf = routes .get (action )
111148 if not conf :
112149 await query .answer ("未知类型" )
@@ -135,19 +172,53 @@ async def callback_broadcast_exec(update: Update, context: ContextTypes.DEFAULT_
135172
136173 success = 0
137174 sent_map = {}
175+ media_group_id = msg_to_media_group .get (msg_id )
176+ media_items = media_group_cache .get (media_group_id , {}).get ("items" ) if media_group_id else None
177+ is_album = media_items and len (media_items ) > 0
138178
139179 for t in targets :
140180 try :
141- sent = await context .bot .copy_message (
142- chat_id = t ["id" ],
143- from_chat_id = ADMIN_ID ,
144- message_id = msg_id ,
145- message_thread_id = t ["topic" ],
146- reply_markup = kb ,
147- protect_content = True
148- )
149- success += 1
150- sent_map [str (t ["id" ])] = sent .message_id
181+ # 相册路径:保持多媒体合并
182+ if is_album :
183+ media_payload = []
184+ for idx , item in enumerate (media_items ):
185+ if not item :
186+ continue
187+ caption = html .escape (item ["caption" ]) if idx == 0 and item .get ("caption" ) else None
188+ if item ["type" ] == "photo" :
189+ media_payload .append (InputMediaPhoto (media = item ["file_id" ], caption = caption , parse_mode = ParseMode .HTML ))
190+ elif item ["type" ] == "video" :
191+ media_payload .append (InputMediaVideo (media = item ["file_id" ], caption = caption , parse_mode = ParseMode .HTML ))
192+ sent_album = await context .bot .send_media_group (
193+ chat_id = t ["id" ],
194+ media = media_payload ,
195+ message_thread_id = t ["topic" ],
196+ protect_content = True
197+ )
198+ success += 1
199+ # 记录第一条映射,便于回复追踪
200+ if sent_album :
201+ sent_map [str (t ["id" ])] = sent_album [0 ].message_id
202+ # 信号类型补发互动按钮
203+ if kb :
204+ await context .bot .send_message (
205+ chat_id = t ["id" ],
206+ text = "📈 信号互动" ,
207+ reply_markup = kb ,
208+ message_thread_id = t ["topic" ],
209+ protect_content = True
210+ )
211+ else :
212+ sent = await context .bot .copy_message (
213+ chat_id = t ["id" ],
214+ from_chat_id = ADMIN_ID ,
215+ message_id = msg_id ,
216+ message_thread_id = t ["topic" ],
217+ reply_markup = kb ,
218+ protect_content = True
219+ )
220+ success += 1
221+ sent_map [str (t ["id" ])] = sent .message_id
151222
152223 # [修复 2] 捕获具体的发送错误,清理死群
153224 except (BadRequest , Forbidden ) as e :
@@ -221,4 +292,4 @@ async def callback_signal_btn(update: Update, context: ContextTypes.DEFAULT_TYPE
221292 else :
222293 await query .answer ("✅ 已记录" )
223294 except Exception :
224- await query .answer ()
295+ await query .answer ()
0 commit comments