背景
地址:https://github.com/huijimuhe/postman
核心就是android的AccessibilityService,回復功能api需要23以上版本才行。
其實很像在做單元測試。你可以有n種方式實現發帖功能,這只是一個比較邪火的方式,親測過一次,可行。這里我以網易新聞客戶端舉例。
模擬你在手機端的物理動作:選擇新聞-》回復-》退回新聞列表-》進入下一個新聞-》回復-》退回新聞列表刷新-》進入-》回復....
做的不精細,只是探究到底可不可行。你可以用在任何APP中自動發消息,只要沒有驗證碼。
你要拿來玩,請抱着一顆開心的心情。
原理
直接在github上開源的微信紅包插件改的,紅包插件項目和你需要了解的幾篇文章在這里
https://github.com/geeeeeeeeek/WeChatLuckyMoney
http://www.xuebuyuan.com/2061597.html
http://www.xuebuyuan.com/2061595.html
http://developer.android.com/training/accessibility/service.html

package com.huijimuhe.pman.services; import android.accessibilityservice.AccessibilityService; import android.content.ComponentName; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.huijimuhe.pman.utils.PowerUtil; import java.util.ArrayList; import java.util.List; public class PostService extends AccessibilityService implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "PostService"; private static final String MAIN_ACT = "MainActivity"; private static final String DETAIL_ACT = "NewsPageActivity"; private static final String BASE_ACT = "BaseActivity"; private static final int MSG_BACK = 159; private static final int MSG_REFRESH_NEW_LIST = 707; private static final int MSG_READ_NEWS = 19; private static final int MSG_POST_COMMENT = 211; private static final int MSG_REFRESH_COMPLETE = 22; private static final int MSG_FINISH_COMMENT = 59; private String currentActivityName = MAIN_ACT; private HandlerEx mHandler = new HandlerEx(); private boolean mIsMutex = false; private int mReadCount = 0; private List<String> readedNews = new ArrayList<>(); private PowerUtil powerUtil; private SharedPreferences sharedPreferences; /** * AccessibilityEvent * * @param event 事件 */ @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (sharedPreferences == null) return; setCurrentActivityName(event); watchMain(event); watchBasic(event); watchDetail(event); } private void watchMain(AccessibilityEvent event) { //新聞列表 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && currentActivityName.contains(MAIN_ACT)) { if (mReadCount > 4) { //如果讀取完了都沒有新的就刷新 Log.d(TAG, "新聞已讀取完,需要刷新列表"); //需要刷新列表了 mHandler.sendEmptyMessage(MSG_REFRESH_NEW_LIST); } else { mHandler.sendEmptyMessage(MSG_READ_NEWS); } } } private void watchDetail(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && currentActivityName.contains(DETAIL_ACT)) { //添加評論 mHandler.sendEmptyMessage(MSG_POST_COMMENT); } } private void watchBasic(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && currentActivityName.contains(BASE_ACT)) { Log.d(TAG, "進入非新聞頁,即將退出"); mHandler.sendEmptyMessage(MSG_BACK); mHandler.sendEmptyMessage(MSG_BACK); } } private void refreshList() { List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("android:id/list"); for (AccessibilityNodeInfo node : nodes) { //頁面是否加載完成 if (node == null) return; //執行刷新 node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } //重新開始讀取新聞 mHandler.sendEmptyMessage(MSG_REFRESH_COMPLETE); } private void enterDetailAct() { //獲取列表items List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/perfect_item"); for (AccessibilityNodeInfo node : nodes) { //頁面是否加載完成 if (node == null) return; //獲取列表item的標題 List<AccessibilityNodeInfo> titles = node.findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/title"); for (AccessibilityNodeInfo title : titles) { //檢查是否已讀取 if (!readedNews.contains(title.getText().toString())) { //點擊讀取該新聞 readedNews.add(title.getText().toString()); node.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); Log.d(TAG, "進入新聞:" + title.getText().toString()); mReadCount++; //進入一個就停止 return; } } } } private void postComment() { //激活輸入框 List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/mock_reply_edit"); for (AccessibilityNodeInfo node : nodes) { //頁面是否加載完成 if (node == null) return; node.performAction(AccessibilityNodeInfo.ACTION_CLICK); } //輸入內容 List<AccessibilityNodeInfo> editNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/reply_edit"); for (AccessibilityNodeInfo node : editNodes) { //頁面是否加載完成 if (node == null) return; Bundle arguments = new Bundle(); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "抽煙的人最討厭了"); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } // //回復按鈕 // List<AccessibilityNodeInfo> postNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/reply"); // for (AccessibilityNodeInfo node : postNodes) { // //頁面是否加載完成 // if (node == null) return; // node.performAction(AccessibilityNodeInfo.ACTION_CLICK); // } //退出 mHandler.sendEmptyMessage(MSG_FINISH_COMMENT); Log.d(TAG, "評論已發表"); } /** * 設置當前頁面名稱 * * @param event */ private void setCurrentActivityName(AccessibilityEvent event) { if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { return; } try { ComponentName componentName = new ComponentName(event.getPackageName().toString(), event.getClassName().toString()); getPackageManager().getActivityInfo(componentName, 0); currentActivityName = componentName.flattenToShortString(); Log.d(TAG, "<--pkgName-->" + event.getPackageName().toString()); Log.d(TAG, "<--className-->" + event.getClassName().toString()); Log.d(TAG, "<--currentActivityName-->" + currentActivityName); } catch (PackageManager.NameNotFoundException e) { currentActivityName = MAIN_ACT; } } @Override public void onDestroy() { this.powerUtil.handleWakeLock(false); super.onDestroy(); } @Override public void onInterrupt() { } @Override public void onServiceConnected() { super.onServiceConnected(); this.watchFlagsFromPreference(); } /** * 屏幕是否常亮 */ private void watchFlagsFromPreference() { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); sharedPreferences.registerOnSharedPreferenceChangeListener(this); this.powerUtil = new PowerUtil(this); Boolean watchOnLockFlag = sharedPreferences.getBoolean("pref_watch_on_lock", false); this.powerUtil.handleWakeLock(watchOnLockFlag); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals("pref_watch_on_lock")) { Boolean changedValue = sharedPreferences.getBoolean(key, false); this.powerUtil.handleWakeLock(changedValue); } } /** * 處理機 */ private class HandlerEx extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { //后退 case MSG_BACK: new Handler().postDelayed(new Runnable() { @Override public void run() { performGlobalAction(GLOBAL_ACTION_BACK); } }, 1000); break; //結束評論 case MSG_FINISH_COMMENT: for (int i = 0; i < 4; i++) { new Handler().postDelayed(new Runnable() { @Override public void run() { performGlobalAction(GLOBAL_ACTION_BACK); } }, 2000 +i*500); } break; //刷新列表 case MSG_REFRESH_NEW_LIST: new Handler().postDelayed(new Runnable() { @Override public void run() { refreshList(); } }, 3000); break; //結束刷新 case MSG_REFRESH_COMPLETE: new Handler().postDelayed(new Runnable() { @Override public void run() { mReadCount = 0; enterDetailAct(); } }, 3000); break; //進入新聞頁 case MSG_READ_NEWS: new Handler().postDelayed(new Runnable() { @Override public void run() { enterDetailAct(); } }, 3000); break; //發送評論 case MSG_POST_COMMENT: new Handler().postDelayed(new Runnable() { @Override public void run() { postComment(); } }, 3000); break; } } } }
在開始寫代碼前,你應該至少閱讀了之前幾篇文章和微信紅包插件的代碼,然后還應該掌握用Android Device Monitor查看UI樹的工具使用。(最近開始研究iOS逆向,這個確實比reveal和cycript方便太多)
粗略實現步驟
1.manifest中申明服務
<service android:name=".services.PostService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService"/> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessible_service_config"/> </service>
2.設定你需要監控的app包名來過濾,在/res/xml/accessible_service_config.xml中
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/app_description" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackAllMask" android:packageNames="com.netease.newsreader.activity" android:notificationTimeout="10" android:settingsActivity="com.huijimuhe.pman.activities.SettingsActivity" android:accessibilityFlags="flagIncludeNotImportantViews|flagDefault" android:canRetrieveWindowContent="true"/>
比如網易的,android:packageNames="com.netease.newsreader.activity"
3.在AccessibleService中實現對事件的監聽
@Override public void onAccessibilityEvent(AccessibilityEvent event) { if (sharedPreferences == null) return; setCurrentActivityName(event); watchMain(event); watchBasic(event); watchDetail(event); } /** * 設置當前頁面名稱 * * @param event */ private void setCurrentActivityName(AccessibilityEvent event) { if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { return; } try { ComponentName componentName = new ComponentName(event.getPackageName().toString(), event.getClassName().toString()); getPackageManager().getActivityInfo(componentName, 0); currentActivityName = componentName.flattenToShortString(); Log.d(TAG, "<--pkgName-->" + event.getPackageName().toString()); Log.d(TAG, "<--className-->" + event.getClassName().toString()); Log.d(TAG, "<--currentActivityName-->" + currentActivityName); } catch (PackageManager.NameNotFoundException e) { currentActivityName = MAIN_ACT; } }
4.監控是否是新聞列表,可以設定個頁面刷新閥值
//新聞列表 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && currentActivityName.contains(MAIN_ACT)) { if (mReadCount > 4) { //如果讀取完了都沒有新的就刷新 Log.d(TAG, "新聞已讀取完,需要刷新列表"); //需要刷新列表了 mHandler.sendEmptyMessage(MSG_REFRESH_NEW_LIST); } else { mHandler.sendEmptyMessage(MSG_READ_NEWS); } }
5.監控是否是新聞詳情
private void watchDetail(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && currentActivityName.contains(DETAIL_ACT)) { //添加評論 mHandler.sendEmptyMessage(MSG_POST_COMMENT); } }
6監控是否廣告或其他專題,不做操作
private void watchBasic(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && currentActivityName.contains(BASE_ACT)) { Log.d(TAG, "進入非新聞頁,即將退出"); mHandler.sendEmptyMessage(MSG_BACK); mHandler.sendEmptyMessage(MSG_BACK); } }
7.回復評論
private void postComment() { //激活輸入框 List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/mock_reply_edit"); for (AccessibilityNodeInfo node : nodes) { //頁面是否加載完成 if (node == null) return; node.performAction(AccessibilityNodeInfo.ACTION_CLICK); } //輸入內容 List<AccessibilityNodeInfo> editNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.netease.newsreader.activity:id/reply_edit"); for (AccessibilityNodeInfo node : editNodes) { //頁面是否加載完成 if (node == null) return; Bundle arguments = new Bundle(); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "抽煙的人最討厭了"); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } //退出 mHandler.sendEmptyMessage(MSG_FINISH_COMMENT); Log.d(TAG, "評論已發表"); }
總體思路是通過postDelay來實現操作的間隔,其他的請自己閱讀代碼,我只測試了下思路是否可行就沒有繼續延伸下去了。
大家不要留言說我簡單事情做那么復雜。用物理方式(現在回頭看倒覺得很像單元測試)實現回復,真實性是100%,發貼機你要倒騰一個別人家服務器看不出作弊的,估計更費勁吧。
如果你覺得python寫腳本很酷或者直接用fiddler抓包然后寫個發帖器都行。我這還有個用Tesseract-OCR做驗證碼識別的winform。
做這個只是當時覺得紅包插件原理很酷,可以有點其他玩法,我也確實倒騰了一個,也開源了https://github.com/huijimuhe/focus
要是開開腦洞,比如不停的微信給欠債老板發消息讓還錢啥的,這種插件倒是很能氣死他,哈哈哈哈。
要搞什么推廣(尤其是賣面膜的)應該靠金主,而不是這個,哈哈哈哈。
P.S.
自己在做獨立開發,希望廣結英豪,尤其是像我一樣腦子短路不用react硬拼anroid、ios原生想干點什么的朋友。App獨立開發群533838427
微信公眾號『懶文』-->lanwenapp<--