Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
abcac89
feat: add retry download capability for media messages
jiz4oh Apr 27, 2025
288cbab
feat: add /retry command to allow manual retry of timed out messages
jiz4oh Apr 27, 2025
802c0a7
fix: handle file failed
jiz4oh Apr 28, 2025
166ada6
fix: fix missing path
jiz4oh Apr 30, 2025
bcc15bd
feat: add log
jiz4oh Apr 30, 2025
42230b7
fix: properly process failed retry
jiz4oh Apr 30, 2025
b0ea18f
fix: remove unsupported share type
jiz4oh Apr 30, 2025
7ecf224
feat: return the prompt after retry failed
jiz4oh May 8, 2025
2f4511b
fix: add share type support
jiz4oh May 8, 2025
1ce2c1d
feat: add progress prompt on download media
jiz4oh May 30, 2025
57fb760
fix: make sure edit message is exists
jiz4oh May 30, 2025
8f72902
fix: fix name 'msgid' is not defined
jiz4oh May 30, 2025
d422a96
feat: try to guess msgtype
jiz4oh May 30, 2025
8f052f5
fix: respect original filename
jiz4oh Jun 1, 2025
a3fabd7
feat: remove progress feature on download media
jiz4oh Jul 19, 2025
a25a5b7
refactor: remove unnecessary codes
jiz4oh Aug 13, 2025
bb0ce4e
Merge branch 'master' into retry
jiz4oh Nov 22, 2025
d799d6c
refactor: extract media message building logic to MsgDeco
jiz4oh Nov 22, 2025
516b889
feat: raise EFBMessageError instead
jiz4oh Nov 22, 2025
c0f0e5a
fix: improve error handling in media retry download
jiz4oh Nov 22, 2025
f323420
fix: incorrect file path
jiz4oh Nov 26, 2025
8698d0d
fix: msg["filepath"] may not exist when sending file message
jiz4oh Dec 2, 2025
4be21a8
fix: remove unnecessary edit flags
jiz4oh Dec 2, 2025
ff8a5b5
fix: file download failure when filepath is directory
jiz4oh Dec 2, 2025
6b72561
fix(comwechat): use dynamic base path for CDN files
jiz4oh Dec 30, 2025
bc4b587
feat: add download process as placeholder for media edits
jiz4oh Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 117 additions & 12 deletions efb_wechat_comwechat_slave/ComWechat.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

import re
import json
from ehforwarderbot.chat import SystemChat, PrivateChat , SystemChatMember, ChatMember, SelfChatMember
import hashlib
from ehforwarderbot.chat import SystemChat, PrivateChat , GroupChat, SystemChatMember, ChatMember, SelfChatMember
from typing import Tuple, Optional, Collection, BinaryIO, Dict, Any , Union , List
from datetime import datetime
from cachetools import TTLCache
Expand All @@ -31,7 +31,7 @@

from .ChatMgr import ChatMgr
from .CustomTypes import EFBGroupChat, EFBPrivateChat, EFBGroupMember, EFBSystemUser
from .MsgDeco import qutoed_text
from .MsgDeco import qutoed_text, rebuild_media_msg
from .MsgProcess import MsgProcess, MsgWrapper
from .Utils import download_file , load_config , load_temp_file_to_local , WC_EMOTICON_CONVERSION
from .db import DatabaseManager
Expand Down Expand Up @@ -515,22 +515,23 @@ def handle_msg(self , msg : Dict[str, Any] , author : 'ChatMember' , chat : 'Cha
msg["timestamp"] = int(time.time())
msg["filepath"] = msg["filepath"].replace("\\","/")
msg["filepath"] = f'''{self.dir}{msg["filepath"]}'''
self.file_msg[msg["filepath"]] = ( msg , author , chat )
self._send_file_msg(msg , author , chat )
return
if msg["type"] == "video":
msg["timestamp"] = int(time.time())
msg["filepath"] = msg["thumb_path"].replace("\\","/").replace(".jpg", ".mp4")
msg["filepath"] = f'''{self.dir}{msg["filepath"]}'''
self.file_msg[msg["filepath"]] = ( msg , author , chat )
self._send_file_msg(msg , author , chat )
return
except:
except Exception as e:
self.logger.warning(f"Failed to process file msg: {e}")
...

if msg["type"] == "voice":
file_path = re.search("clientmsgid=\"(.*?)\"", msg["message"]).group(1) + ".amr"
msg["timestamp"] = int(time.time())
msg["filepath"] = f'''{self.dir}{msg["self"]}/{file_path}'''
self.file_msg[msg["filepath"]] = ( msg , author , chat )
self._send_file_msg(msg , author , chat )
return

self.send_efb_msgs(MsgWrapper(msg, MsgProcess(msg, chat)), author=author, chat=chat, uid=MessageID(str(msg['msgid'])))
Expand All @@ -543,14 +544,26 @@ def handle_file_msg(self):
for path in list(self.file_msg.keys()):
flag = False
msg = self.file_msg[path][0]
author = self.file_msg[path][1]
chat = self.file_msg[path][2]
author: ChatMember = self.file_msg[path][1]
chat : Chat= self.file_msg[path][2]
commands = []
msg_type = msg["type"]
if os.path.exists(path):
flag = True
elif (int(time.time()) - msg["timestamp"]) > self.time_out:
msg_type = msg["type"]
msg['message'] = f"[{msg_type} 下载超时,请在手机端查看]"
msg["type"] = "text"
commands.append(
MessageCommand(
name=("Retry"),
callable_name="retry_download",
kwargs={
"msgid": msg["msgid"],
"msgtype": msg_type,
"chatuid": chat.uid,
},
)
)
flag = True
elif msg["type"] == "voice":
sql = f'SELECT Buf FROM Media WHERE Reserved0 = {msg["msgid"]}'
Expand All @@ -564,8 +577,14 @@ def handle_file_msg(self):
flag = True

if flag:
m = MsgProcess(msg, chat)
m.edit = True
m.edit_media = True
if commands:
m.commands = MessageCommands(commands)
m.vendor_specific["wechat_msgtype"] = msg_type
del self.file_msg[path]
self.send_efb_msgs(MsgWrapper(msg, MsgProcess(msg, chat)), author=author, chat=chat, uid=MessageID(str(msg['msgid'])))
self.send_efb_msgs(MsgWrapper(msg, m), author=author, chat=chat, uid=MessageID(str(msg['msgid'])))

if len(self.delete_file):
for k in list(self.delete_file.keys()):
Expand All @@ -578,6 +597,67 @@ def handle_file_msg(self):
pass
del self.delete_file[file_path]

def _send_file_msg(self, msg: Message, author: ChatMember, chat: Chat):
if msg["filepath"] == self.dir:
self.logger.warning(f"Wrong message {msg['msgid']} at {msg['filepath']}.")
text = f"download {msg['type']} failed"
efb_msg = Message(
type=MsgType.Text,
text=text,
commands = MessageCommands([
MessageCommand(
name=("Retry"),
callable_name="retry_download",
kwargs={
"msgid": msg["msgid"],
"msgtype": msg["type"],
"chatuid": chat.uid,
},
)
])
)
self.send_efb_msgs(efb_msg, author=author, chat=chat, uid=MessageID(str(msg['msgid'])))
else:
msg_type = msg['type']
text = f"{msg_type} is downloading, please wait..."
efb_msg = Message(
type=MsgType.Text,
text=text
)
self.send_efb_msgs(efb_msg, author=author, chat=chat, uid=MessageID(str(msg['msgid'])))
self.file_msg[msg["filepath"]] = ( msg , author , chat )

def retry_download(self, msgid, chatuid, msgtype):
path = self.GetMsgCdn(msgid)
if not path:
return "[下载失败]"
chat = self.get_chat(chatuid)
efb_msgs = rebuild_media_msg(msgtype, path)
if not efb_msgs:
return f"[不支持的文件类型: {msgtype}]"
master_message = coordinator.master.get_message_by_id(chat=chat, msg_id=msgid)
self.send_efb_msgs(efb_msgs, uid=msgid, author=master_message.author, chat=master_message.chat, edit=True, edit_media=True)
return "下载成功"

def retry_download_target(self, target: Message = None):
path = self.GetMsgCdn(target.uid)
if not path:
raise EFBMessageError("[重试失败,请在手机端查看,可通过 /retry 回复本条消息再次重试]")
msgtype = target.vendor_specific.get("wechat_msgtype", None)
if not msgtype:
if target.type == MsgType.Image:
msgtype = "image"
elif target.type == MsgType.File:
msgtype = "share"
elif target.type == MsgType.Voice:
msgtype = "voice"
elif target.type == MsgType.Video:
msgtype = "video"
efb_msgs = rebuild_media_msg(msgtype, path)
author = target.author
chat = target.chat
self.send_efb_msgs(efb_msgs, uid=target.uid, author=author, chat=chat, edit=True, edit_media=True)

def process_friend_request(self , v3 , v4):
self.logger.debug(f"process_friend_request:{v3} {v4}")
res = self.bot.VerifyApply(v3 = v3 , v4 = v4)
Expand Down Expand Up @@ -749,8 +829,7 @@ def send_message(self, msg : Message) -> Message:
msg.target.text = text
self.send_efb_msgs(msg.target, edit=True)
else:
text = f"无法转发{msgid},不是有效的微信消息"
self.system_msg({'sender': chat_uid, 'message': text, 'target': msg.target})
raise EFBMessageError(f"无法转发{msgid},不是有效的微信消息")
return msg
elif msg.text.startswith('/at'):
users_message = msg.text[4::].split(' ', 1)
Expand All @@ -765,6 +844,12 @@ def send_message(self, msg : Message) -> Message:
res = self.bot.SendAt(chatroom_id = chat_uid, wxids = users, msg = message)
else:
self.bot.SendText(wxid = chat_uid , msg = msg.text)
elif msg.text.startswith('/retry'):
if isinstance(msg.target, Message):
self.retry_download_target(target=msg.target)
return msg
else:
raise EFBMessageError("回复超时消息时使用")
elif msg.text.startswith('/sendcard'):
user_nickname = msg.text[10::].split(' ', 1)
if len(user_nickname) == 2:
Expand Down Expand Up @@ -935,6 +1020,26 @@ def get_name_by_wxid(self, wxid):
name = wxid
return name

def GetMsgCdn(self, msgid):
try:
res = self.bot.GetCdn(msgid=msgid)
if res["msg"] == 1:
path = res["path"].replace(self.bot.get_base_path() + "\\", self.dir).replace("\\","/")
count = 1
while True:
if os.path.exists(path):
break
elif count > 12: # telegram 超过 15s 会报错
self.logger.warning(f"Timeout when retrying download {msgid} at {path}.")
return
count += 1
time.sleep(1)

self.logger.debug(f"Download {path} successfully.")
return path
except Exception as e:
self.logger.warning(f"Error occurred when retrying download {msgid}. {e}")

@staticmethod
def non_blocking_lock_wrapper(lock: threading.Lock) :
def wrapper(func):
Expand Down
21 changes: 21 additions & 0 deletions efb_wechat_comwechat_slave/MsgDeco.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Mapping, Tuple, List, Union, IO
import magic
import os
from lxml import etree
from functools import partial
from traceback import print_exc
Expand All @@ -12,6 +13,7 @@

from .ChatMgr import ChatMgr
from .CustomTypes import EFBGroupChat, EFBPrivateChat
from .Utils import *

QUOTE_DIVIDER = " - - - - - - - - - - - - - - - "

Expand Down Expand Up @@ -78,6 +80,25 @@ def parse_chat_history(xml, level: int = 1) -> list[dict]:
res.append(data)
return res

def rebuild_media_msg(msgtype, path):
if not path:
return
filename = os.path.basename(path)
if msgtype == "image":
file = wechatimagedecode(path)
return efb_image_wrapper(file)
elif msgtype == "share":
file = load_local_file_to_temp(path)
return efb_file_wrapper(file, filename=filename)
elif msgtype == "voice":
file = load_local_file_to_temp(path)
return efb_voice_wrapper(convert_silk_to_mp3(file) , file.name + ".ogg")
elif msgtype == "video":
file = load_local_file_to_temp(path)
return efb_video_wrapper(file, filename=filename)
else:
return

def efb_text_simple_wrapper(text: str, ats: Union[Mapping[Tuple[int, int], Union[Chat, ChatMember]], None] = None) -> Message:
"""
A simple EFB message wrapper for plain text. Emojis are presented as is (plain text).
Expand Down