├── .gitattributes
├── .gitignore
├── README.md
├── assets
├── flask-weixin-output.png
├── weixin-simulator-gui.png
└── weixin-simulator-output.png
├── gui.py
├── requirements.txt
└── screenshot.png
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | # Build results
45 | [Dd]ebug/
46 | [Rr]elease/
47 | *_i.c
48 | *_p.c
49 | *.ilk
50 | *.meta
51 | *.obj
52 | *.pch
53 | *.pdb
54 | *.pgc
55 | *.pgd
56 | *.rsp
57 | *.sbr
58 | *.tlb
59 | *.tli
60 | *.tlh
61 | *.tmp
62 | *.vspscc
63 | .builds
64 | *.dotCover
65 |
66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this
67 | #packages/
68 |
69 | # Visual C++ cache files
70 | ipch/
71 | *.aps
72 | *.ncb
73 | *.opensdf
74 | *.sdf
75 |
76 | # Visual Studio profiler
77 | *.psess
78 | *.vsp
79 |
80 | # ReSharper is a .NET coding add-in
81 | _ReSharper*
82 |
83 | # Installshield output folder
84 | [Ee]xpress
85 |
86 | # DocProject is a documentation generator add-in
87 | DocProject/buildhelp/
88 | DocProject/Help/*.HxT
89 | DocProject/Help/*.HxC
90 | DocProject/Help/*.hhc
91 | DocProject/Help/*.hhk
92 | DocProject/Help/*.hhp
93 | DocProject/Help/Html2
94 | DocProject/Help/html
95 |
96 | # Click-Once directory
97 | publish
98 |
99 | # Others
100 | [Bb]in
101 | [Oo]bj
102 | sql
103 | TestResults
104 | *.Cache
105 | ClientBin
106 | stylecop.*
107 | ~$*
108 | *.dbmdl
109 | Generated_Code #added for RIA/Silverlight projects
110 |
111 | # Backup & report files from converting an old project file to a newer
112 | # Visual Studio version. Backup files are not needed, because we have git ;-)
113 | _UpgradeReport_Files/
114 | Backup*/
115 | UpgradeLog*.XML
116 |
117 |
118 |
119 | ############
120 | ## Windows
121 | ############
122 |
123 | # Windows image file caches
124 | Thumbs.db
125 |
126 | # Folder config file
127 | Desktop.ini
128 |
129 |
130 | #############
131 | ## Python
132 | #############
133 |
134 | *.py[co]
135 |
136 | # Packages
137 | *.egg
138 | *.egg-info
139 | dist
140 | build
141 | eggs
142 | parts
143 | bin
144 | var
145 | sdist
146 | develop-eggs
147 | .installed.cfg
148 |
149 | # Installer logs
150 | pip-log.txt
151 |
152 | # Unit test / coverage reports
153 | .coverage
154 | .tox
155 |
156 | #Translations
157 | *.mo
158 |
159 | #Mr Developer
160 | .mr.developer.cfg
161 |
162 | # Mac crap
163 | .DS_Store
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weixin-simulator
2 |
3 | 微信公众平台没有本地调试环境,在开发时遇到了很多麻烦。写测试固然是一种方式,但维护一大批测试样例挺费神的,所以希望能有什么东西模拟微信客户端在本地与应用进行交互。求谷歌不得,于是用`tkinter`自己写了个模拟器。
4 |
5 |
6 | ## 配置
7 | 请根据需要在主文件`gui.py`中修改settings字典,最重要的是把`url`修改为相应Handler的URL。
8 | ```python
9 | settings = {
10 | # `ToUserName` & `FromUserName` will be placed in the XML data posted to
11 | # the given URL.
12 | "ToUserName": "gh_bea8cf2a04fd",
13 | "FromUserName": "oLXjgjiWeAS1gfe4ECchYewwoyTc",
14 |
15 | # URL of your Wexin handler.
16 | "url": "http://localhost:8080/weixin",
17 |
18 | # These will be displayed in GUI.
19 | "mp_display_name": "APP",
20 | "me_display_name": "ME",
21 |
22 | # The token you submitted to Weixin MP. Used to generate signature.
23 | "token": ""
24 | }
25 | ```
26 |
27 | ## 使用
28 | 完成设置后直接运行主文件`gui.py`即可。效果如下图:
29 |
30 | 
31 |
32 | 点击`关注公众帐号`将模拟一位微信用户关注该公众帐号。
33 |
34 | 点击`取消关注`将模拟一位微信用户取消关注该公众帐号。
35 |
36 | 发送`c@EVENT_KEY`将模拟一位微信用户点击以`EVENT_KEY`为`EventKey`的`CLICK`类型自定义菜单选项。
37 |
38 | 发送`v@URL`将模拟一位微信用户点击以`URL`为`EventKey`的`VIEW`类型自定义菜单选项。
39 |
40 | ## 其他
41 | 功能很简单也很不完善,不过最基本的开发够用了。好在代码也很简单,请随意修改以满足自己的需求。欢迎发PR。
42 |
43 |
44 | ## Example: Interact with Flask-Weixin example
45 |
46 | [Flask-Weixin](https://github.com/lepture/flask-weixin)
47 |
48 | 
49 |
50 | 
51 |
52 | 
53 |
--------------------------------------------------------------------------------
/assets/flask-weixin-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushuz/weixin-simulator/c396244a0eba51c7389673462eca619af1425fbf/assets/flask-weixin-output.png
--------------------------------------------------------------------------------
/assets/weixin-simulator-gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushuz/weixin-simulator/c396244a0eba51c7389673462eca619af1425fbf/assets/weixin-simulator-gui.png
--------------------------------------------------------------------------------
/assets/weixin-simulator-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushuz/weixin-simulator/c396244a0eba51c7389673462eca619af1425fbf/assets/weixin-simulator-output.png
--------------------------------------------------------------------------------
/gui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import time
4 | import random
5 | import hashlib
6 | import xml.etree.cElementTree as ET
7 |
8 | import tkinter as tk
9 | from tkinter import scrolledtext as st
10 |
11 | import requests
12 |
13 |
14 | settings = {
15 | # `ToUserName` & `FromUserName` will be placed in the XML data posted to
16 | # the given URL.
17 | "ToUserName": "gh_bea8cf2a04fd",
18 | "FromUserName": "oLXjgjiWeAS1gfe4ECchYewwoyTc",
19 |
20 | # URL of your Wexin handler.
21 | "url": "http://localhost:8080/weixin",
22 | # Path to client certificate
23 | # Put certificate and secret key into a single PEM file
24 | "cert": "",
25 |
26 | # These will be displayed in GUI.
27 | "mp_display_name": "APP",
28 | "me_display_name": "ME",
29 |
30 | # The token you submitted to Weixin MP. Used to generate signature.
31 | "token": "B0e8alq5ZmMjcnG5gwwLRPW2",
32 | }
33 |
34 |
35 | TPL_TEXT = '''
36 |
37 |
38 |
39 | %(time)d
40 |
41 |
42 | %(id)s
43 |
44 | '''
45 |
46 | TPL_EVENT = '''
47 |
48 |
49 |
50 | %(time)d
51 |
52 |
53 |
54 | '''
55 |
56 |
57 | def post(qs, data):
58 | headers = {'Content-Type': 'text/xml'}
59 | r = requests.post(settings["url"] + qs, data=data.encode(), cert=settings["cert"], verify=False, headers=headers)
60 | return r.content
61 |
62 |
63 | def send():
64 | s = e.get()
65 |
66 | if not s:
67 | return
68 |
69 | # Simulation for clicking the menu.
70 | # Usage: EVENT_TYPE@EVENT_KEY
71 | # 'c@KEY_FIND' - 'CLICK' event with 'KEY_FIND' as event key
72 | # 'v@www.qq.com' - 'VIEW' event with 'www.qq.com' as event key
73 | if s.startswith("c@") or s.startswith("v@"):
74 | event, key = s.split("@")
75 | msg = {
76 | "to": settings["ToUserName"],
77 | "from": settings["FromUserName"],
78 | "time": time.time(),
79 | "event": "CLICK" if event == "c" else "VIEW",
80 | "key": key
81 | }
82 | qs = "?signature=%s×tamp=%s&nonce=%s" % \
83 | mix(int(msg["time"]))
84 | receive(msg["time"], post(qs, TPL_EVENT % msg))
85 | # Simulation for sending a message.
86 | else:
87 | t.insert(tk.END, settings["me_display_name"]+"\n", "send_name")
88 | t.insert(tk.END, s+"\n", "send_content")
89 |
90 | msg = {
91 | "to": settings["ToUserName"],
92 | "from": settings["FromUserName"],
93 | "time": time.time(),
94 | "content": s,
95 | "id": str(random.random())[-10:]
96 | }
97 |
98 | qs = "?signature=%s×tamp=%s&nonce=%s" % \
99 | mix(int(msg["time"]))
100 | receive(msg["time"], post(qs, TPL_TEXT % msg))
101 |
102 |
103 | def receive(start, response):
104 | if time.time() - start > 4.95:
105 | return
106 |
107 | if not response:
108 | print("No response.")
109 | return
110 |
111 | print("Received:\n%s\n" % response.decode())
112 | try:
113 | et = ET.fromstring(response)
114 | except ET.ParseError:
115 | print("Bad XML.")
116 | return
117 |
118 | to = et.find("ToUserName").text
119 | fr = et.find("FromUserName").text
120 |
121 | type = et.find("MsgType").text
122 | if type == "text":
123 | c = et.find("Content").text
124 | elif type == "news":
125 | l = ["[news]"]
126 | for i in et.find("Articles").findall("item"):
127 | l.append("Title={0}".format(i.find("Title").text))
128 | l.append("Description={0}".format(i.find("Description").text))
129 | l.append("PicUrl={0}".format(i.find("PicUrl").text))
130 | l.append("Url={0}".format(i.find("Url").text))
131 | l.append("---")
132 | c = "\n".join(l)
133 | elif type == "image":
134 | c = "[image]\nMediaId={0}\n---".format(et.find("Image").find("MediaId").text)
135 | else:
136 | print("Unknown response.")
137 | return
138 |
139 | t.insert(tk.END, settings["mp_display_name"]+"\n", "receive_name")
140 | t.insert(tk.END, c+"\n", "receive_content")
141 | t.yview_moveto(1.0)
142 |
143 |
144 | def mix(time):
145 | timestamp = str(time)
146 |
147 | # I don't know how Weixin generate the 9-digit nonce, so I turn to random.
148 | nonce = str(int(random.random()))[-9:]
149 |
150 | l = [timestamp, nonce, settings["token"]]
151 | l.sort()
152 | signature = hashlib.sha1("".join(l).encode()).hexdigest()
153 |
154 | return signature, timestamp, nonce
155 |
156 |
157 | def follow():
158 | msg = {
159 | "to": settings["ToUserName"],
160 | "from": settings["FromUserName"],
161 | "time": time.time(),
162 | "event": "subscribe",
163 | "key": ""
164 | }
165 | qs = "?signature=%s×tamp=%s&nonce=%s" % \
166 | mix(int(msg["time"]))
167 | receive(msg["time"], post(qs, TPL_EVENT % msg))
168 |
169 |
170 | def unfollow():
171 | msg = {
172 | "to": settings["ToUserName"],
173 | "from": settings["FromUserName"],
174 | "time": time.time(),
175 | "event": "unsubscribe",
176 | "key": "" # `EventKey` in `unsubscribe` event is empty.
177 | }
178 | qs = "?signature=%s×tamp=%s&nonce=%s" % \
179 | mix(int(msg["time"]))
180 | receive(msg["time"], post(qs, TPL_EVENT % msg))
181 |
182 |
183 | top = tk.Tk()
184 | top.title("微信模拟器")
185 |
186 | t = st.ScrolledText(top, width=45, height=30)
187 | t.pack()
188 |
189 | t.tag_add("send_name", "1.0", "1.end")
190 | t.tag_config("send_name", font=("Arial", "10", "bold"), spacing3=5, rmargin=6,
191 | justify=tk.RIGHT)
192 | t.tag_add("send_content", "2.0", "2.end")
193 | t.tag_config("send_content", spacing3=2, justify=tk.RIGHT, rmargin=6)
194 |
195 | t.tag_add("receive_name", "1.0", "1.end")
196 | t.tag_config("receive_name", font=("Arial", "10", "bold"), spacing3=5, lmargin1=2)
197 | t.tag_add("receive_content", "2.0", "2.end")
198 | t.tag_config("receive_content", lmargin1=2)
199 |
200 | e = tk.Entry(top)
201 | e.pack(side=tk.LEFT)
202 |
203 | b = tk.Button(top, text="发送", command=send)
204 | b.pack(side=tk.LEFT)
205 |
206 | a = tk.Button(top, text="关注公众帐号", command=follow)
207 | a.pack(side=tk.RIGHT)
208 |
209 | a = tk.Button(top, text="取消关注", command=unfollow)
210 | a.pack(side=tk.RIGHT)
211 |
212 | if __name__ == "__main__":
213 | top.mainloop()
214 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushuz/weixin-simulator/c396244a0eba51c7389673462eca619af1425fbf/screenshot.png
--------------------------------------------------------------------------------