feat: add dashboard friend circle management

This commit is contained in:
liuwei
2026-04-07 12:50:50 +08:00
parent d507cdf88d
commit e8ed0d4799
7 changed files with 1073 additions and 63 deletions

View File

@@ -1,4 +1,5 @@
from wechat_ipad import UserLoggedOut
from wechat_ipad.client.friend_circle import FriendCircleMixin
from wechat_ipad.client.firends import FriendMixin
from wechat_ipad.client.group import ChatroomMixin
from wechat_ipad.client.login import LoginMixin
@@ -7,7 +8,7 @@ from wechat_ipad.client.tools import ToolMixin
from wechat_ipad.client.user import UserMixin
class WechatAPIClient(LoginMixin, MessageMixin, FriendMixin, ChatroomMixin, UserMixin,
class WechatAPIClient(LoginMixin, MessageMixin, FriendCircleMixin, FriendMixin, ChatroomMixin, UserMixin,
ToolMixin):
# 这里都是需要结合多个功能的方法

View File

@@ -0,0 +1,145 @@
import base64
import os
from typing import Union
import aiofiles
import aiohttp
from wechat_ipad import UserLoggedOut
from wechat_ipad.client.base import WechatAPIClientBase
from wechat_ipad.models.friend_circle_info import build_friend_circle_xml
class FriendCircleMixin(WechatAPIClientBase):
async def get_friend_circle_list(self, max_id: int = 0, first_page_md5: str = "") -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
async with aiohttp.ClientSession() as session:
json_param = {"Wxid": self.wxid, "Maxid": max_id, "Fristpagemd5": first_page_md5}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/GetList", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def get_friend_circle_detail(self, towxid: str, max_id: int = 0, first_page_md5: str = "") -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
async with aiohttp.ClientSession() as session:
json_param = {
"Wxid": self.wxid,
"Towxid": towxid,
"Maxid": max_id,
"Fristpagemd5": first_page_md5
}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/GetDetail", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def get_friend_circle_id_detail(self, object_id: Union[str, int], towxid: str = "") -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
async with aiohttp.ClientSession() as session:
json_param = {"Wxid": self.wxid, "Towxid": towxid, "Id": int(object_id)}
response = await session.post(
f"http://{self.ip}:{self.port}/api/FriendCircle/GetIdDetail",
json=json_param
)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def publish_friend_circle(self, content: str, media_items: list[dict] | None = None,
blacklist: str = "", with_user_list: str = "") -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
xml_content = build_friend_circle_xml(self.wxid, content, media_items=media_items)
async with aiohttp.ClientSession() as session:
json_param = {
"Wxid": self.wxid,
"Content": xml_content,
"BlackList": blacklist,
"WithUserList": with_user_list
}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/Messages", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def friend_circle_comment(self, object_id: str, content: str = "", type: int = 2,
reply_comment_id: int = 0) -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
async with aiohttp.ClientSession() as session:
json_param = {
"Wxid": self.wxid,
"Id": str(object_id),
"Type": int(type),
"Content": content,
"ReplyCommnetId": int(reply_comment_id)
}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/Comment", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def friend_circle_operation(self, object_id: str, type: int, comment_id: int = 0) -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
async with aiohttp.ClientSession() as session:
json_param = {
"Wxid": self.wxid,
"Id": str(object_id),
"Type": int(type),
"CommnetId": int(comment_id)
}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/Operation", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def sync_friend_circle(self, sync_key: str = "") -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
async with aiohttp.ClientSession() as session:
json_param = {"Wxid": self.wxid, "Synckey": sync_key}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/MmSnsSync", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)
async def upload_friend_circle_media(self, media: Union[str, bytes, os.PathLike]) -> dict:
if not self.wxid:
raise UserLoggedOut("请先登录")
if isinstance(media, str):
media_base64 = media.split(",", 1)[1] if "," in media else media
elif isinstance(media, bytes):
media_base64 = base64.b64encode(media).decode()
elif isinstance(media, os.PathLike):
async with aiofiles.open(media, "rb") as f:
media_base64 = base64.b64encode(await f.read()).decode()
else:
raise ValueError("media should be str, bytes, or path")
async with aiohttp.ClientSession() as session:
json_param = {"Wxid": self.wxid, "Base64": media_base64}
response = await session.post(f"http://{self.ip}:{self.port}/api/FriendCircle/Upload", json=json_param)
json_resp = await response.json()
if json_resp.get("Success"):
return json_resp.get("Data", {})
self.error_handler(json_resp)

View File

@@ -1,25 +1,65 @@
FRIEND_CIRCLE_INFO = """
import random
import time
from typing import Iterable
def _safe_float(value) -> float:
try:
return float(value or 0)
except (TypeError, ValueError):
return 0.0
def generate_timeline_id() -> str:
return f"{int(time.time() * 1000)}{random.randint(1000, 9999)}"
def build_media_xml(media_items: Iterable[dict]) -> str:
parts = []
for index, item in enumerate(media_items):
url = item.get("url", "")
thumb = item.get("thumb", url)
media_id = item.get("id") or f"{int(time.time() * 1000)}{index}"
md5 = item.get("md5", "")
total_size = item.get("total_size", 0)
width = item.get("width", 0)
height = item.get("height", 0)
parts.append(
f"""
<media>
<id><![CDATA[{media_id}]]></id>
<type><![CDATA[2]]></type>
<title></title>
<description></description>
<private><![CDATA[0]]></private>
<url type="1" md5="{md5}"><![CDATA[{url}]]></url>
<thumb type="1"><![CDATA[{thumb}]]></thumb>
<videoDuration><![CDATA[0.0]]></videoDuration>
<size totalSize="{_safe_float(total_size):.1f}" width="{_safe_float(width):.1f}" height="{_safe_float(height):.1f}"></size>
</media>
""".strip()
)
return "\n".join(parts)
def build_friend_circle_xml(wxid: str, content: str, media_items: list[dict] | None = None,
timeline_id: str | None = None, create_time: int | None = None) -> str:
media_items = media_items or []
timeline_id = timeline_id or generate_timeline_id()
create_time = create_time or int(time.time())
content_style = 1 if media_items else 2
media_xml = build_media_xml(media_items)
return f"""
<TimelineObject>
<id>
<![CDATA[{id}]]>
</id>
<username>
<![CDATA[{wxid}]]>
</username>
<createTime>
<![CDATA[{time}]]>
</createTime>
<id><![CDATA[{timeline_id}]]></id>
<username><![CDATA[{wxid}]]></username>
<createTime><![CDATA[{create_time}]]></createTime>
<contentDescShowType>0</contentDescShowType>
<contentDescScene>0</contentDescScene>
<private>
<![CDATA[0]]>
</private>
<contentDesc>
<![CDATA[{content}]]>
</contentDesc>
<contentattr>
<![CDATA[0]]>
</contentattr>
<private><![CDATA[0]]></private>
<contentDesc><![CDATA[{content}]]></contentDesc>
<contentattr><![CDATA[0]]></contentattr>
<sourceUserName></sourceUserName>
<publicUserName></publicUserName>
<sourceNickName></sourceNickName>
@@ -27,58 +67,24 @@ FRIEND_CIRCLE_INFO = """
<weappInfo>
<appUserName></appUserName>
<pagePath></pagePath>
<version>
<![CDATA[0]]>
</version>
<version><![CDATA[0]]></version>
<isHidden>0</isHidden>
<debugMode>
<![CDATA[0]]>
</debugMode>
<debugMode><![CDATA[0]]></debugMode>
<shareActionId></shareActionId>
<isGame>
<![CDATA[0]]>
</isGame>
<isGame><![CDATA[0]]></isGame>
<messageExtraData></messageExtraData>
<subType>
<![CDATA[0]]>
</subType>
<subType><![CDATA[0]]></subType>
<preloadResources></preloadResources>
</weappInfo>
<canvasInfoXml></canvasInfoXml>
<ContentObject>
<contentStyle>
<![CDATA[1]]>
</contentStyle>
<contentSubStyle>
<![CDATA[0]]>
</contentSubStyle>
<contentStyle><![CDATA[{content_style}]]></contentStyle>
<contentSubStyle><![CDATA[0]]></contentSubStyle>
<title></title>
<description></description>
<contentUrl></contentUrl>
<mediaList>
<media>
<id>
<![CDATA[14672447414385119864]]>
</id>
<type>
<![CDATA[2]]>
</type>
<title></title>
<description></description>
<private>
<![CDATA[0]]>
</private>
<url type=\"1\" md5=\"c661215f338618b3282a6ea5174a0fb5\">
<![CDATA[http://szmmsns.qpic.cn/mmsns/AcIhsXSWkDeNF6K5icia87OmccBSE2sDH5gib5UWibjTCU1snn0WANibQoy4Obad1ibmQO6aNQXOTP4Fc/0]]>
</url>
<thumb type=\"1\">
<![CDATA[http://szmmsns.qpic.cn/mmsns/AcIhsXSWkDeNF6K5icia87OmccBSE2sDH5gib5UWibjTCU1snn0WANibQoy4Obad1ibmQO6aNQXOTP4Fc/150]]>
</thumb>
<videoDuration>
<![CDATA[0.0]]>
</videoDuration>
<size totalSize=\"47880.0\" width=\"1080.0\" height=\"1080.0\"></size>
</media>
{media_xml}
</mediaList>
</ContentObject>
<actionInfo>
@@ -91,11 +97,11 @@ FRIEND_CIRCLE_INFO = """
<appInfo>
<id></id>
</appInfo>
<location poiClassifyId=\"\" poiName=\"\" poiAddress=\"\" poiClassifyType=\"0\" city=\"\"></location>
<location poiClassifyId="" poiName="" poiAddress="" poiClassifyType="0" city=""></location>
<streamvideo>
<streamvideourl></streamvideourl>
<streamvideothumburl></streamvideothumburl>
<streamvideoweburl></streamvideoweburl>
</streamvideo>
</TimelineObject>
"""
""".strip()