通过把脚本挂到服务器上,自此告别手动挡,不用去手动转发小宇宙,
不用手动加好友,然后把别人一个个拉到我的Py交易群里。正当我
暗自窃喜的时候,微信并没有放过我这只小猫咪。
我还记得那天早上,我兴高采烈早早来到公司,更新了一波代码准备为
我的机器人添砖加瓦的时候,当我关闭了阿里云上的脚本,这时候意外来了,
我的机器人小号,再也无法通过微信网页端的接口登录了!!!
扫描完二维码,永远提示的都是下面这样一句话:
<error><ret>1203</ret><message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。
你可以通过Windows微信、Mac微信或者手机客户端微信登录。</message></error>
1
2
是的,就是这样一句话,找不到申诉渠道,也不知道何时才可能会解封。(客户端任可正常使用)
而现在另外新申请的微信小号是无法登录微信网页端的,其实这是微信在慢慢关停网页版登录,
最主要的原因就是机器人泛滥!
没有了网页版微信,日子还是要过的,难道只能回归手动档么?几种解决方案:
1.研究客户端协议(这个成本巨高,而且官方稍微改点东西,够你哭的)
2.APP逆向,利用Xposed框架,hook相关的方法,也是有些研究成本的;
3.利用类似与按键精灵的东西,编写脚本让他自动点点点,自动化测试
工具或者本节讲的这个无障碍服务——AccessibilityService
AccessibilityService其实不是一个新的东西了,老久之前就有了,
官方原意:优化残障人士的使用体验的,而在我大天朝:
抢红包,自动安装,一键XXX等等,可谓欣欣向荣。
使用AccessibilityService也非常Easy,核心要点就是:
通过UI Automator找到节点,通过resource-id,text,content-desc等
唯一特征定位到具体的节点,接着执行各种模拟操作,点,滚动,填充,
用法比较简单的,大部分时间会花在试错和逻辑调整上!
来一发通过AccessibilityService实现的自动加好友以及拉人进群聊的Gif体验下:
Gif加速了一点,不过完成加好友以及拉人总共也就耗时15s,是相当客观的啦。
下面就来介绍下AccessibilityService这个玩意怎么用吧~
AccessibilityService用法简介
1.自定义Service继承AccessibilityService
如题,自定义一个AccessibilityService类,重写两个主要方法:
onInterrupt( ):辅助功能中断的回调,基本不用理,核心还是
onAccessibilityEvent(AccessibilityEvent event) 上。
当界面发生了什么事情,比如顶部Notification,界面更新,内容变化等,
会触发这个方法,你可以根据不同的事件响应不同的操作,比如小猪这个
就是当顶部出现加好友的Notification的event时,跳转到加好友页。
点开AccessibilityEvent类可以看到一堆的事件类型~
事件类型 描述
TYPE_VIEW_CLICKED View被点击
TYPE_VIEW_LONG_CLICKED View被长按
TYPE_VIEW_SELECTED View被选中
TYPE_VIEW_FOCUSED View获得焦点
TYPE_VIEW_TEXT_CHANGED View文本变化
TYPE_WINDOW_STATE_CHANGED 打开了一个PopupWindow,Menu或Dialog
TYPE_NOTIFICATION_STATE_CHANGED Notification变化
TYPE_VIEW_HOVER_ENTER 一个View进入悬停
TYPE_VIEW_HOVER_EXIT 一个View退出悬停
TYPE_TOUCH_EXPLORATION_GESTURE_START 触摸浏览事件开始
TYPE_TOUCH_EXPLORATION_GESTURE_END 触摸浏览事件完成
TYPE_WINDOW_CONTENT_CHANGED 窗口的内容发生变化,或子树根布局发生变化
TYPE_VIEW_SCROLLED View滚动
TYPE_VIEW_TEXT_SELECTION_CHANGED Edittext文字选中发生改变事件
TYPE_ANNOUNCEMENT 应用产生一个通知事件
TYPE_VIEW_ACCESSIBILITY_FOCUSED 获得无障碍焦点事件
TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED 无障碍焦点事件清除
TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY 在给定的移动粒度下遍历视图文本的事件
TYPE_GESTURE_DETECTION_START 开始手势监测
TYPE_GESTURE_DETECTION_END 结束手势监测
TYPE_TOUCH_INTERACTION_START 触摸屏幕事件开始
TYPE_TOUCH_INTERACTION_END 触摸屏幕事件结束
TYPE_WINDOWS_CHANGED 屏幕上的窗口变化事件,需要API 21+
TYPE_VIEW_CONTEXT_CLICKED View中的上下文点击事件
TYPE_ASSIST_READING_CONTEXT 辅助用户读取当前屏幕事件
好吧,上面的表其实并没什么大用,我还是习惯直接把event.toString()给打印出来,
然后自行去判断~
如图就可以拿到event类型,以及产生对应事件的类名,核心是这两个,
除此之外还有Text和ContentDescription等。
比如我那个监听Notification跳转到添加好友页的:
这里就是对事件类型做了下判断,然后获取contentIntent,跳转而已。
简单点讲就是:
你在这个方法里,去判断一波事件类型和className,
然后再获取控件,做一些点击,滚动,填充文本等。
2.服务的配置
自定义完这个服务要想让他启用你还得执行下面的操作:
Step 1:在res文件夹下创建xml文件夹,新建一个配置的xml文件(名字自己定)
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:notificationTimeout="100"
android:packageNames="com.tencent.mm"
android:settingsActivity="com.coderpig.wechathelper.MainActivity" />
1
2
3
4
5
6
7
8
9
属性简介如下:
accessibilityEventTypes:设置监听的事件种类,用|隔开,监听所有可以用typeAllMask;
accessibilityFeedbackType:服务提供的反馈类型,feedbackGeneric通用反馈;
accessibilityFlags:辅助功能附加的标志,flagDefault默认的配置
canRetrieveWindowContent:辅助功能服务是否能够取回活动窗口内容的属性
notificationTimeout:响应时间
packageNames:监听的应用包名,不填,默认监听所有应用的事件
settingsActivity:允许用户修改辅助功能的activity类名
Step 2:接着AndroidManifest.xml文件中对该Service进行配置
先是添加一个权限:
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
1
接着是Service的配置:
这里是你那个配置文件xml文件的文件名,其他照抄。
Step 3:安装到手机后,需要在手机设置的无障碍处开启服务
一般在设置的辅助功能处能找到:
如果Logcat那里能看到打印的LOG,说明服务正常运行,接下来要找控件节点
3.找控件
这里可以用到神器UI Automator来查看布局层次,打开Android Studio,
Ctrl + alt + A,输入 monitor
依次点击:选中设备 -> Dump View Hierarchy for UI Automator
稍等一会,右侧就会出现当前页面的布局层次图,如图随手选中一个邀请的节点:
右侧可以拿到对应的信息,一般比较常用的是这几个,有一点要注意!!!
resource-id不一定是唯一的
获得控件基本都会通过下述这个方法:
getRootInActiveWindow( ):获取当前整个活动窗口的根节点
返回的是一个AccessibilityNodeInfo类,代表View的状态信息,
提供了下述几个非常实用的方法:
getParent:获取父节点。
getChild:获取子节点。
performAction:在节点上执行一个动作。
findAccessibilityNodeInfosByText:通过字符串查找节点元素。
findAccessibilityNodeInfosByViewId:通过视图id查找节点元素。
后面的这两个方法会返回一个AccessibilityNodeInfo列表,一般操作是
遍历,然后筛选特定节点,比如我程序里的,获得底部Tab节点为”通讯录”,
然后点击,跳转后遍历,筛选”群聊”的节点,点击。
另外,UI Automator有时并不可靠(实时问题),我建议写多一个遍历节点
的方法,可以更清楚里面的控件情况:
拿到控件,接着就到触发事件了。
4.触发事件
通过调用performAction()传入一个时间类型即可触发相应时间,比如点击,长按等
事件就多了,自己点开AccessibilityNodeInfo类查看吧,这里介绍下最常用的几个事件:
//点击
performAction(AccessibilityNodeInfo.ACTION_CLICK);
//长按
performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
//滚动
performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); //向下滚一下
performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); //向上滚一下
//填充EditText(API版本需要>18可用方法1,API>21两种方法都可以使用)
//方法1:
ClipboardManager clipboard = (ClipboardManager)this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text", "填充内容");
clipboard.setPrimaryClip(clip);
//获得焦点
info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
粘贴进入内容
info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
//方法2:
Bundle arguments = new Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "填充内容");
info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
除了控件触发事件外,AccessibilityService提供了一个performGlobalAction(),用于执行
一些通用的事件:
GLOBAL_ACTION_BACK 点击返回按钮
GLOBAL_ACTION_HOME 点击home
GLOBAL_ACTION_NOTIFICATIONS 打开通知
GLOBAL_ACTION_RECENTS 打开最近应用
GLOBAL_ACTION_QUICK_SETTINGS 打开快速设置
GLOBAL_ACTION_POWER_DIALOG 打开长按电源键的弹框
1
2
3
4
5
6
另外在实际开发中,直接调用这些全局方法又是并没有生效,
我在调GLOBAL_ACTION_BACK的时候就发现有时不会回退,
个人的解决方案是使用handler.postDelay()延时执行:
除了这样玩以外,我还利用时间差,串行去执行几个任务,比如:
上面的步骤是:
进入群聊聊天信息页后,列表滚动两次,接着依次:
1.延时1s后,找到添加成员按钮并点击;
2.延时2.3s后,把名字填充到EditText里
3.延时3s后,点击确定按钮
就不用过于依赖onAccessibilityEvent方法,除了用handler.postDelay外,
还可以用Thread.sleep(休眠时长),用到的点大概就这么多,其余的自行探究吧。
小结
本节讲解一波如何通过AccessibilityService来实现自动加好友以及拉人进群,
之前是打算用xposed来写的,后面发现没我想像中简单,而且很多用安卓机的都
不会搞机(基),root也不会,后来还是选择了AccessibilityService,简单易用,
当然后面还是会研究一波xposed实现的,敬请期待~
对了,还有,之前那个网页端的机器人被封原因估计是信息秒回,如果有还用
itchat那个做机器人的,建议回复的时间可以稍微延长些;
关于AccessibilityService更多内容可见:
Android辅助功能:https://blog.csdn.net/qq_24800377/article/details/78283662
Building Accessibility Services:https://developer.android.com/guide/topics/ui/accessibility/services.html
Developing an Accessibility Service:https://developer.android.com/training/accessibility/service.html
附:关键代码(都可以在:https://github.com/coder-pig/WechatHelper 找到):
代码有Bug的话正常,后续会优化下逻辑,感觉写得有点杂~
package com.coderpig.wechathelper;
import android.accessibilityservice.AccessibilityService;
import android.app.Notification;
import android.app.PendingIntent;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.List;
/**
* 描述:微信监控服务类
*
* @author CoderPig on 2018/04/04 13:46.
*/
public class HelperService extends AccessibilityService {
private static final String TAG = "HelperService";
private Handler handler = new Handler();
private String userName = "123";
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
CharSequence classNameChr = event.getClassName();
String className = classNameChr.toString();
Log.d(TAG, event.toString());
switch (eventType) {
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
Notification notification = (Notification) event.getParcelableData();
String content = notification.tickerText.toString();
if (content.contains("请求添加你为朋友")) {
PendingIntent pendingIntent = notification.contentIntent;
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
}
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
switch (className) {
case "com.tencent.mm.plugin.subapp.ui.friend.FMessageConversationUI":
addFriend();
break;
case "com.tencent.mm.plugin.profile.ui.SayHiWithSnsPermissionUI":
verifyFriend();
break;
case "com.tencent.mm.plugin.profile.ui.ContactInfoUI":
performBackClick();
break;
case "com.tencent.mm.ui.LauncherUI":
if (!userName.equals("123")) {
openGroup();
}
break;
case "com.tencent.mm.ui.contact.ChatroomContactUI":
if (!userName.equals("123")) {
inviteGroup();
}
break;
case "com.tencent.mm.ui.chatting.ChattingUI":
if (!userName.equals("123")) {
openGroupSetting();
}
break;
case "com.tencent.mm.plugin.chatroom.ui.ChatroomInfoUI":
if (userName.equals("123")) {
performBackClick();
} else {
addToGroup();
}
break;
case "com.tencent.mm.ui.base.i":
dialogClick();
break;
}
break;
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
}
}
private void addFriend() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> list = nodeInfo
.findAccessibilityNodeInfosByText("接受");
if (list != null && list.size() > 0) {
for (AccessibilityNodeInfo n : list) {
n.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
} else {
performBackClick();
}
}
}
private void verifyFriend() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
//获得用户名
if (nodeInfo != null) {
userName = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/d0n").get(0).getText().toString();
AccessibilityNodeInfo finishNode = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/hd").get(0);
finishNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
private void openGroup() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/ca5");
for (AccessibilityNodeInfo info : nodes) {
if (info.getText().toString().equals("通讯录")) {
info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
handler.postDelayed(new Runnable() {
@Override
public void run() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j5");
for (AccessibilityNodeInfo info : nodes) {
if (info.getText().toString().equals("群聊")) {
info.getParent().getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
}
}
}
}, 500L);
}
}
}
}
private void inviteGroup() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/a9v");
for (AccessibilityNodeInfo info : nodes) {
if (info.getText().toString().equals("小猪的Python学习交流群")) {
info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
}
}
}
private void openGroupSetting() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/he").get(0).performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
private void addToGroup() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> listNodes = nodeInfo.findAccessibilityNodeInfosByViewId("android:id/list");
if(listNodes != null && listNodes.size() > 0) {
AccessibilityNodeInfo listNode = listNodes.get(0);
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
final AccessibilityNodeInfo scrollNodeInfo = getRootInActiveWindow();
if (scrollNodeInfo != null) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> nodes = scrollNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/d0b");
for (AccessibilityNodeInfo info : nodes) {
if (info.getContentDescription().toString().equals("添加成员")) {
info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
}
}
},1000L);
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> editNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/arz");
if(editNodes != null && editNodes.size() > 0) {
AccessibilityNodeInfo editNode = editNodes.get(0);
Bundle arguments = new Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, userName);
editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
}
}
}, 2300L);
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> cbNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/kr");
if(cbNodes != null) {
AccessibilityNodeInfo cbNode = null;
if(cbNodes.size() == 1) {
cbNode = cbNodes.get(0);
} else if(cbNodes.size() == 2) {
cbNode = cbNodes.get(1);
}
if (cbNode != null) {
cbNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
AccessibilityNodeInfo sureNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/hd").get(0);
sureNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
}, 3000L);
}
}
}
}
private void dialogClick() {
AccessibilityNodeInfo inviteNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aln").get(0);
inviteNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
userName = "123";
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> sureNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aln");
if(sureNodes != null && sureNodes.size() > 0) {
AccessibilityNodeInfo sureNode = sureNodes.get(0);
sureNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
},1000L);
}
private void performBackClick() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
}
}, 300L);
}
//遍历控件的方法
public void recycle(AccessibilityNodeInfo info) {
if (info.getChildCount() == 0) {
Log.i(TAG, "child widget----------------------------" + info.getClassName().toString());
Log.i(TAG, "showDialog:" + info.canOpenPopup());
Log.i(TAG, "Text:" + info.getText());
Log.i(TAG, "windowId:" + info.getWindowId());
Log.i(TAG, "desc:" + info.getContentDescription());
} else {
for (int i = 0; i < info.getChildCount(); i++) {
if (info.getChild(i) != null) {
recycle(info.getChild(i));
}
}
}
}
@Override
public void onInterrupt() {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
来啊,Py交易啊
想加群一起学习Py的可以加下,智障机器人小Pig,验证信息里包含:
Python,python,py,Py,加群,交易,屁眼 中的一个关键词即可通过;
————————————————
版权声明:本文为CSDN博主「coder-pig」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/coder_pig/article/details/79871063