├── README.md ├── down.py ├── exception.py ├── mgr.py ├── models.py ├── ntchat-flask.py ├── utils.py ├── xdg.py └── xdg.pyc /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmmya/ntchat-httpapi/6f90876bc68dd8f4f8e628d9ce68f0a2471dc03e/README.md -------------------------------------------------------------------------------- /down.py: -------------------------------------------------------------------------------- 1 | #正雨 @ 1695960757 2 | import os.path 3 | import time 4 | import requests 5 | from xdg import get_download_dir 6 | from models import SendMediaReqModel 7 | 8 | 9 | def get_local_path(datax): 10 | if "file_path" in datax.keys(): 11 | if os.path.isfile(datax["file_path"]): 12 | return datax["file_path"] 13 | if "url" not in datax.keys(): 14 | return None 15 | data = requests.get(datax["url"]).content 16 | temp_file = os.path.join(get_download_dir(), str(time.time_ns())) 17 | with open(temp_file, 'wb') as fp: 18 | fp.write(data) 19 | return temp_file 20 | 21 | 22 | -------------------------------------------------------------------------------- /exception.py: -------------------------------------------------------------------------------- 1 | #正雨 @ 1695960757 2 | class ClientNotExists(Exception): 3 | guid = "" 4 | 5 | def __init__(self, guid): 6 | self.guid = guid 7 | 8 | 9 | class MediaNotExistsError(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /mgr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #正雨 @ 1695960757 3 | import ntchat 4 | import requests 5 | import threading 6 | from typing import Dict, Union 7 | from ntchat.utils.singleton import Singleton 8 | from utils import generate_guid 9 | from exception import ClientNotExists 10 | 11 | 12 | class ClientWeChat(ntchat.WeChat): 13 | guid: str = "" 14 | qrcode_event: threading.Event = None 15 | qrcode: str = "" 16 | 17 | 18 | class ClientManager(metaclass=Singleton): 19 | __client_map: Dict[str, ntchat.WeChat] = {} 20 | callback_url: str = "" 21 | 22 | def new_guid(self): 23 | """ 24 | 生成新的guid 25 | """ 26 | while True: 27 | guid = generate_guid("wechat") 28 | if guid not in self.__client_map: 29 | return guid 30 | # 创建实例 31 | def create_client(self): 32 | guid = self.new_guid() 33 | wechat = ClientWeChat() 34 | wechat.guid = guid 35 | self.__client_map[guid] = wechat 36 | 37 | # 注册回调 38 | wechat.on(ntchat.MT_ALL, self.__on_callback) 39 | wechat.on(ntchat.MT_RECV_WECHAT_QUIT_MSG, self.__on_quit_callback) 40 | return guid 41 | # 返回实例 42 | def get_client(self, guid: str) -> Union[None, ntchat.WeChat]: 43 | client = self.__client_map.get(guid, None) 44 | if client is None: 45 | raise ClientNotExists(guid) 46 | return client 47 | # 删除实例 48 | def remove_client(self, guid): 49 | if guid in self.__client_map: 50 | del self.__client_map[guid] 51 | return self.__client_map 52 | 53 | 54 | def __on_callback(self, wechat: ClientWeChat, message: dict): 55 | 56 | # 通知二维码显示 57 | msg_type = message['type'] 58 | if msg_type == ntchat.MT_RECV_LOGIN_QRCODE_MSG and wechat.qrcode_event: 59 | wechat.qrcode = message["data"]["code"] 60 | wechat.qrcode_event.set() 61 | 62 | if not self.callback_url: 63 | return 64 | 65 | client_message = { 66 | "guid": wechat.guid, 67 | "message": message 68 | } 69 | requests.post(self.callback_url, json=client_message) 70 | 71 | def __on_quit_callback(self, wechat): 72 | self.__on_callback(wechat, {"type": ntchat.MT_RECV_WECHAT_QUIT_MSG, "data": {}}) 73 | 74 | # 返回实例字典 75 | def get_guid_dict(self): 76 | return self.__client_map -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | #正雨 @ 1695960757 2 | from typing import Optional, List, Any, Union, Dict 3 | from pydantic import BaseModel 4 | 5 | 6 | class ClientReqModel(BaseModel): 7 | guid: str 8 | 9 | 10 | class ResponseModel(BaseModel): 11 | status: int 12 | msg: Optional[str] = "" 13 | data: Optional[Any] = None 14 | 15 | 16 | class ClientOpenReqModel(ClientReqModel): 17 | smart: Optional[bool] = True 18 | show_login_qrcode: Optional[bool] = False 19 | 20 | 21 | class CallbackUrlReqModel(BaseModel): 22 | callback_url: Optional[str] = "" 23 | 24 | 25 | class UserProfileModel(BaseModel): 26 | wxid: str 27 | nickname: str 28 | account: str 29 | avatar: str 30 | 31 | 32 | class ContactModel(BaseModel): 33 | account: str 34 | avatar: str 35 | city: str 36 | country: str 37 | nickname: str 38 | province: str 39 | remark: str 40 | sex: int 41 | wxid: str 42 | 43 | 44 | class ContactDetailReqModel(ClientReqModel): 45 | wxid: str 46 | 47 | 48 | class ContactDetailModel(BaseModel): 49 | account: str 50 | avatar: str 51 | city: str 52 | country: str 53 | nickname: str 54 | province: str 55 | remark: str 56 | sex: int 57 | wxid: str 58 | signature: str 59 | small_avatar: str 60 | sns_pic: str 61 | source_type: int 62 | status: int 63 | v1: str 64 | v2: str 65 | 66 | 67 | class AcceptFriendReqModel(ClientReqModel): 68 | encryptusername: str 69 | ticket: str 70 | scene: int 71 | 72 | 73 | class RoomModel(BaseModel): 74 | wxid: str 75 | nickname: str 76 | avatar: str 77 | is_manager: int 78 | manager_wxid: str 79 | total_member: int 80 | member_list: List[str] 81 | 82 | 83 | class RoomMemberModel(ContactModel): 84 | display_name: str 85 | 86 | 87 | class GetRoomMembersReqModel(ClientReqModel): 88 | room_wxid: str 89 | 90 | 91 | class GetRoomNameReqModel(ClientReqModel): 92 | room_wxid: str 93 | 94 | 95 | class CreateRoomReqModel(ClientReqModel): 96 | member_list: List[str] 97 | 98 | 99 | class RoomMembersReqModel(CreateRoomReqModel): 100 | room_wxid: str 101 | 102 | 103 | class AddRoomFriendReqModel(ClientReqModel): 104 | room_wxid: str 105 | wxid: str 106 | verify: str 107 | 108 | 109 | class RoomReqModel(ClientReqModel): 110 | room_wxid: str 111 | 112 | 113 | class ModifyRoomNameReqModel(RoomReqModel): 114 | name: str 115 | 116 | 117 | class SendMsgReqModel(ClientReqModel): 118 | to_wxid: str 119 | 120 | 121 | class SendTextReqModel(SendMsgReqModel): 122 | content: str 123 | 124 | 125 | class SendRoomAtReqModel(SendTextReqModel): 126 | at_list: List[str] 127 | 128 | 129 | class SendCardReqModel(SendMsgReqModel): 130 | card_wxid: str 131 | 132 | 133 | class SendLinkCardReqModel(SendMsgReqModel): 134 | title: str 135 | desc: str 136 | url: str 137 | image_url: str 138 | 139 | 140 | class SendMediaReqModel(SendMsgReqModel): 141 | file_path: Optional[str] = "" 142 | url: Optional[str] = "" 143 | 144 | 145 | class SendXmlReqModel(SendMsgReqModel): 146 | xml: str 147 | 148 | 149 | class SendPatReqModel(ClientReqModel): 150 | room_wxid: str 151 | patted_wxid: str 152 | 153 | 154 | class ModifyFriendRemarkReqModel(ClientReqModel): 155 | wxid: str 156 | remark: str -------------------------------------------------------------------------------- /ntchat-flask.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #正雨 @ 1695960757 3 | 4 | from flask import Flask,request 5 | from functools import wraps 6 | from mgr import ClientManager 7 | from down import get_local_path 8 | from exception import MediaNotExistsError, ClientNotExists 9 | import os 10 | import json,threading 11 | os.environ['NTCHAT_LOG'] = "ERROR" 12 | import ntchat 13 | 14 | 15 | def response_json(status=500, data=None, msg=""): 16 | return { 17 | "status": status, 18 | "data": {} if data is None else data, 19 | "msg": msg 20 | } 21 | 22 | 23 | class catch_exception: 24 | def __call__(self, f): 25 | @wraps(f) 26 | async def wrapper(*args, **kwargs): 27 | try: 28 | return await f(*args, **kwargs) 29 | except ntchat.WeChatNotLoginError: 30 | return response_json(msg="wechat instance not login") 31 | except ntchat.WeChatBindError: 32 | return response_json(msg="wechat bind error") 33 | except ntchat.WeChatVersionNotMatchError: 34 | return response_json(msg="wechat version not match, install require wechat version") 35 | except MediaNotExistsError: 36 | return response_json(msg="file_path or url error") 37 | except ClientNotExists as e: 38 | return response_json(msg="client not exists, guid: %s" % e.guid) 39 | except Exception as e: 40 | return response_json(msg=str(e)) 41 | 42 | return wrapper 43 | 44 | 45 | client_mgr = ClientManager() 46 | app = Flask(__name__) 47 | 48 | # 设置默认上报地址 49 | client_mgr.callback_url = "http://127.0.0.1:8005/callback" 50 | @app.route('/callback', methods=['get','post']) 51 | def on_callback(): 52 | data = request.stream.read() 53 | data = json.loads(data.decode('utf-8')) 54 | 55 | if data["message"]["type"] == 11025: 56 | print("登录成功!") 57 | print(data) 58 | 59 | 60 | return '' 61 | 62 | #创建实例 63 | @app.route("/client/create",methods=["GET","POST"]) 64 | @catch_exception() 65 | async def client_create(): 66 | guid = client_mgr.create_client() 67 | 68 | return response_json(200, {"guid": guid}) 69 | 70 | #打开微信 71 | @app.route("/client/open", methods=["GET","POST"]) 72 | @catch_exception() 73 | async def client_open(): 74 | datax = request.get_json() 75 | client = client_mgr.get_client(datax["guid"]) 76 | ret = client.open(datax["smart"],datax["show_login_qrcode"]) 77 | # 当show_login_qrcode=True时, 打开微信时会显示二维码界面 78 | if datax["show_login_qrcode"] : 79 | client.qrcode_event = threading.Event() 80 | client.qrcode_event.wait(timeout=10) 81 | 82 | return response_json(200 if ret else 500, {"guid": datax["guid"],'qrcode': client.qrcode}) 83 | 84 | #获取实例列表 85 | @app.route("/client/get_guid_dict", methods=["GET","POST"]) 86 | async def get_guid_dict(): 87 | ret = client_mgr.get_guid_dict() 88 | data = list(ret.keys()) 89 | return response_json(200 if ret else 500,data,"已创建实例") 90 | 91 | #删除实例 92 | @app.route("/client/remove_client", methods=["GET","POST"]) 93 | @catch_exception() 94 | async def remove_client(): 95 | datax = request.get_json() 96 | ret = client_mgr.remove_client(datax["guid"]) 97 | data = list(ret.keys()) 98 | return response_json(200,data) 99 | 100 | #设置接收通知地址 101 | @app.route("/global/set_callback_url", methods=["GET","POST"]) 102 | @catch_exception() 103 | async def client_set_callback_url(): 104 | datax = request.get_json() 105 | client_mgr.callback_url = datax["callback_url"] 106 | return response_json(200) 107 | 108 | #获取登录信息 109 | @app.route("/user/get_login_info", methods=["GET","POST"]) 110 | @catch_exception() 111 | async def user_get_login_info(): 112 | datax = request.get_json() 113 | data = client_mgr.get_client(datax["guid"]).get_login_info() 114 | return response_json(200, data) 115 | 116 | #获取自己的信息 117 | @app.route("/user/get_profile", methods=["GET","POST"]) 118 | @catch_exception() 119 | async def user_get_profile(): 120 | datax = request.get_json() 121 | data = client_mgr.get_client(datax["guid"]).get_self_info() 122 | return response_json(200, data) 123 | 124 | #获取联系人列表 125 | @app.route("/contact/get_contacts",methods=["GET","POST"]) 126 | @catch_exception() 127 | async def get_contacts(): 128 | datax = request.get_json() 129 | data = client_mgr.get_client(datax["guid"]).get_contacts() 130 | return response_json(200, data) 131 | 132 | 133 | #获取指定联系人详细信息 134 | @app.route("/contact/get_contact_detail",methods=["GET","POST"]) 135 | @catch_exception() 136 | async def get_contact_detail(): 137 | datax = request.get_json() 138 | data = client_mgr.get_client(datax["guid"]).get_contact_detail(datax["wxid"]) 139 | return response_json(200, data) 140 | 141 | 142 | 143 | 144 | #获取关注公众号列表 145 | @app.route("/publics/get_publics",methods=["GET","POST"]) 146 | @catch_exception() 147 | async def get_publics(): 148 | datax = request.get_json() 149 | data = client_mgr.get_client(datax["guid"]).get_publics() 150 | return response_json(200, data) 151 | 152 | 153 | #获取群列表 154 | @app.route("/room/get_rooms",methods=["GET","POST"]) 155 | @catch_exception() 156 | async def get_rooms(): 157 | datax = request.get_json() 158 | data = client_mgr.get_client(datax["guid"]).get_rooms() 159 | return response_json(200, data) 160 | 161 | #获取群成员列表 162 | @app.route("/room/get_room_members", methods=["GET","POST"]) 163 | @catch_exception() 164 | async def get_room_members(): 165 | datax = request.get_json() 166 | data = client_mgr.get_client(datax["guid"]).get_room_members(datax["room_wxid"]) 167 | return response_json(200, data) 168 | 169 | #创建群 170 | @app.route("/room/create_room", methods=["GET","POST"]) 171 | @catch_exception() 172 | async def create_room(): 173 | datax = request.get_json() 174 | ret = client_mgr.get_client(datax["guid"]).create_room(datax["member_list"]) 175 | return response_json(200 if ret else 500) 176 | 177 | #添加好友入群 178 | @app.route("/room/add_room_member", methods=["GET","POST"]) 179 | @catch_exception() 180 | async def add_room_member(): 181 | datax = request.get_json() 182 | data = client_mgr.get_client(datax["guid"]).add_room_member(datax["room_wxid"],datax["member_list"]) 183 | return response_json(200, data) 184 | 185 | #邀请好友入群 186 | @app.route("/room/invite_room_member", methods=["GET","POST"]) 187 | @catch_exception() 188 | async def invite_room_member(): 189 | datax = request.get_json() 190 | data = client_mgr.get_client(datax["guid"]).invite_room_member(datax["room_wxid"], datax["member_list"]) 191 | return response_json(200, data) 192 | 193 | #删除群成员 194 | @app.route("/room/del_room_member", methods=["GET","POST"]) 195 | @catch_exception() 196 | async def del_room_member(): 197 | datax = request.get_json() 198 | data = client_mgr.get_client(datax["guid"]).del_room_member(datax["room_wxid"], datax["member_list"]) 199 | return response_json(200, data) 200 | 201 | #添加群成员为好友 202 | @app.route("/room/add_room_friend", methods=["GET","POST"]) 203 | @catch_exception() 204 | async def add_room_friend(): 205 | datax = request.get_json() 206 | data = client_mgr.get_client(datax["guid"]).add_room_friend(datax["room_wxid"], 207 | datax["wxid"], 208 | datax["verify"]) 209 | return response_json(200, data) 210 | 211 | #修改群名 212 | @app.route("/room/modify_name", methods=["GET","POST"]) 213 | @catch_exception() 214 | async def modify_name(): 215 | datax = request.get_json() 216 | data = client_mgr.get_client(datax["guid"]).modify_room_name(datax["room_wxid"],datax["name"]) 217 | return response_json(200, data) 218 | 219 | #修改群公告 220 | @app.route("/room/modify_room_notice", methods=["GET","POST"]) 221 | @catch_exception() 222 | async def modify_room_notice(): 223 | datax = request.get_json() 224 | data = client_mgr.get_client(datax["guid"]).modify_room_notice(datax["room_wxid"],datax["notice"]) 225 | return response_json(200, data) 226 | 227 | 228 | #退出群 229 | @app.route("/room/quit_room", methods=["GET","POST"]) 230 | @catch_exception() 231 | async def quit_room(): 232 | datax = request.get_json() 233 | data = client_mgr.get_client(datax["guid"]).quit_room(datax["room_wxid"]) 234 | return response_json(200, data) 235 | 236 | #发送文本消息 237 | @app.route("/msg/send_text",methods=["GET","POST"]) 238 | @catch_exception() 239 | async def msg_send_text(): 240 | datax = request.get_json() 241 | print(datax) 242 | ret = client_mgr.get_client(datax["guid"]).send_text(datax["to_wxid"], datax["content"]) 243 | return response_json(200 if ret else 500) 244 | 245 | #发送群@消息 246 | @app.route("/msg/send_room_at", methods=["GET","POST"]) 247 | @catch_exception() 248 | async def send_room_at(): 249 | datax = request.get_json() 250 | ret = client_mgr.get_client(datax["guid"]).send_room_at_msg(datax["to_wxid"], 251 | datax["content"], 252 | datax["at_list"]) 253 | return response_json(200 if ret else 500) 254 | 255 | #发送名片 256 | @app.route("/msg/send_card", methods=["GET","POST"]) 257 | @catch_exception() 258 | async def send_card(): 259 | datax = request.get_json() 260 | ret = client_mgr.get_client(datax["guid"]).send_card(datax["to_wxid"], 261 | datax["card_wxid"]) 262 | return response_json(200 if ret else 500) 263 | 264 | #发送链接卡片消息 265 | @app.route("/msg/send_link_card", methods=["GET","POST"]) 266 | @catch_exception() 267 | async def send_link_card(): 268 | datax = request.get_json() 269 | ret = client_mgr.get_client(datax["guid"]).send_link_card(datax["to_wxid"], 270 | datax["title"], 271 | datax["desc"], 272 | datax["url"], 273 | datax["image_url"]) 274 | return response_json(200 if ret else 500) 275 | 276 | #发送图片 277 | @app.route("/msg/send_image", methods=["GET","POST"]) 278 | @catch_exception() 279 | async def send_image(): 280 | datax = request.get_json() 281 | file_path = get_local_path(datax) 282 | if file_path is None: 283 | raise MediaNotExistsError() 284 | ret = client_mgr.get_client(datax["guid"]).send_image(datax["to_wxid"], file_path) 285 | 286 | return response_json(200 if ret else 500) 287 | 288 | #发送文件 289 | @app.route("/msg/send_file",methods=["GET","POST"]) 290 | @catch_exception() 291 | async def send_file(): 292 | datax = request.get_json() 293 | file_path = get_local_path(datax) 294 | if file_path is None: 295 | raise MediaNotExistsError() 296 | ret = client_mgr.get_client(datax["guid"]).send_file(datax["to_wxid"], file_path) 297 | return response_json(200 if ret else 500) 298 | 299 | #发送视频 300 | @app.route("/msg/send_video", methods=["GET","POST"]) 301 | @catch_exception() 302 | async def send_video(): 303 | datax = request.get_json() 304 | file_path = get_local_path(datax) 305 | if file_path is None: 306 | raise MediaNotExistsError() 307 | ret = client_mgr.get_client(datax["guid"]).send_video(datax["to_wxid"], file_path) 308 | return response_json(200 if ret else 500) 309 | 310 | #发送GIF 311 | @app.route("/msg/send_gif",methods=["GET","POST"]) 312 | @catch_exception() 313 | async def send_gif(): 314 | datax = request.get_json() 315 | file_path = get_local_path(datax) 316 | if file_path is None: 317 | raise MediaNotExistsError() 318 | ret = client_mgr.get_client(datax["guid"]).send_gif(datax["to_wxid"], file_path) 319 | return response_json(200 if ret else 500) 320 | 321 | #同意加好友请求 322 | @app.route("/msg/accept_friend_request",methods=["GET","POST"]) 323 | @catch_exception() 324 | async def accept_friend_request(): 325 | datax = request.get_json() 326 | ret = client_mgr.get_client(datax["guid"]).accept_friend_request(datax["encryptusername"], datax["ticket"],datax["scene"]) 327 | return response_json(200 if ret else 500) 328 | 329 | #发送XML原始消息 330 | @app.route("/msg/send_xml",methods=["GET","POST"]) 331 | @catch_exception() 332 | async def send_xml(): 333 | datax = request.get_json() 334 | ret = client_mgr.get_client(datax["guid"]).send_xml(datax["to_wxid"],datax["xml"]) 335 | return response_json(200 if ret else 500) 336 | 337 | #发送拍一拍 338 | @app.route("/msg/send_pat", methods=["GET","POST"]) 339 | @catch_exception() 340 | async def send_pat(): 341 | datax = request.get_json() 342 | data = client_mgr.get_client(datax["guid"]).send_pat(datax["room_wxid"], datax["patted_wxid"]) 343 | return response_json(200, data) 344 | 345 | 346 | 347 | 348 | if __name__ == '__main__': 349 | app.debug = False # 设置调试模式,生产模式的时候要关掉debug 350 | app.run(host='0.0.0.0',port=8005) -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | #正雨 @ 1695960757 2 | import uuid 3 | import time 4 | 5 | 6 | def generate_guid(prefix=''): 7 | return str(uuid.uuid3(uuid.NAMESPACE_URL, prefix + str(time.time()))) 8 | -------------------------------------------------------------------------------- /xdg.py: -------------------------------------------------------------------------------- 1 | #正雨 @ 1695960757 2 | import os 3 | import sys 4 | import os.path 5 | import shutil 6 | 7 | def del_file(path): 8 | if not os.listdir(path): 9 | pass 10 | else: 11 | for i in os.listdir(path): 12 | path_file = os.path.join(path,i) #取文件绝对路径 13 | if os.path.isfile(path_file): 14 | os.remove(path_file) 15 | else: 16 | del_file(path_file) 17 | shutil.rmtree(path_file) 18 | 19 | def get_exec_dir(): 20 | return os.path.dirname(sys.argv[0]) 21 | 22 | 23 | def get_download_dir(): 24 | user_dir = os.path.join(get_exec_dir(), 'download') 25 | user_dir = os.path.abspath(user_dir) 26 | if not os.path.isdir(user_dir): 27 | os.makedirs(user_dir) 28 | del_file(user_dir) 29 | return user_dir -------------------------------------------------------------------------------- /xdg.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmmya/ntchat-httpapi/6f90876bc68dd8f4f8e628d9ce68f0a2471dc03e/xdg.pyc --------------------------------------------------------------------------------