微信,让生活更美好 插件,让微信更方便

不得不说,微信现在已经成为了生活中至关重要的一个工具。在使用的时候难免会想要一些个性化的功能,但是这些功能原版app内没有提供,那么就要靠自己动手了。由于iOSOpenDev不再更新引起的很多不便和搞事情前配置各种麻烦的命令让我都快放弃逆向了,恰巧前段时间看到逆向社区中的大大们在iOSOpenDev基础上整出了个更加好用的MonkeyDev,这个新的Dev让我又有了玩一把微信的心思。

使用MonkeyDev将以前的一些tweak放进去编译之后可以完美的跑在手机上了,现在逆向真的是有点傻瓜操作了,只需要关心如何将自己的想法hook出来,其余的繁复操作都交给Dev就好了。那好了,这次就好好体验一下MonkeyDev来写一个微信助手的tweak,hook的思路会一步一步的列出来,涉及到的主要代码会贴出来,功能都是一些有实际需求但不知道可不可以实现的😜,80%在探索后都可以实现,不然这篇文章就是在扯淡了。

涉及到的功能包括但不会详细探究的:各种姿势抢红包、防止消息撤回、修改微信步数、群聊黑名单,因为这些功能都已经有很详细的教程讲解了,并且如果微信不改版,基本的hook思路是不会变的。虽然不会涉及到这些,但是也会将前人的思路整理列出来,以供查阅学习。另外针对于修改微信步数,我做了一个更加有趣的可玩功能,虽然是改步数,但是也要改的不那么明显。

here we go.

以下功能都是在微信 6.5.16 版本下进行实现的

1.自动抢红包

思路大致为:hook收到红包的消息—>进行红包解析—>模拟执行抢的操作

简书:一步步实现微信抢红包(非越狱)

简书:微信红包实现原理

Github:Tweak源码

2.防止消息撤回

思路为:hook收到【撤回消息】的消息,不执行删除消息的操作,然后添加一个本地消息提示阻止了一条消息撤回。

简书:Mac版微信防消息撤回 文章中有github地址

3.群聊黑名单

大致思路为:使用一个数组记录选择的黑名单到本地,hook获取消息的方法,如果是群聊消息且在数组中,就直接返回。

[Github:Tweak源码](https://github.com/buginux/WeChatRedEnvelop

文章:如何在逆向工程中 Hook 得更准 - 微信屏蔽好友&群消息实战

4.花样修改微信步数

所有的修改微信步数的操作都是在获取在微信步数的时候hook,然后展示想要的数字。

对比这种太过痕迹化的改步数,在修改步数的时候增加一些可玩性的操作,让修改步数不那么明显。除了基础的固定步数,主要的玩法有两个:与『第一名』、『最后一名』、『任意一名』多/少n步;选择排行榜中的某一个人比他多/少n步。这些都是逻辑的处理,没什么可说的,看官也可以按照自己的想法编写一套逻辑来修改步数。

主要的难点在于获取加入微信运动排行榜的所有用户供选择,这个既然微信运动排行榜中有,那么就可以获取到。

获取微信运动好友列表:在排行榜中获取好友列表的时候是使用的主动请求,具体的如何请求都放在了DeviceRankSnsMgr类里面,所以可以使用这个类进行模拟获取微信运动好友列表。请求排行榜是通过这个类的- (void)getUserRankListCount:(id)arg1 chanpionUsername:(id)arg2 brandUserName:(id)arg3方法发出的,对于这个请求的响应是- (void)handleRankGetUserRankLikeResponse:(ProtobufCGIWrap *)arg1回调,其中ProtobufCGIWrap的有用信息如下:

1
2
3
4
5
@interface ProtobufCGIWrap : NSObject 
@property(retain, nonatomic) GetUserRankLikeCountResponse *m_pbResponse; // @synthesize m_pbResponse;
@property(retain, nonatomic) GetUserRankLikeCountRequest *m_pbRequest; // @synthesize m_pbRequest;
@property(nonatomic) unsigned int m_uiCgi; // @synthesize m_uiCgi;
@end

很明显,m_pbResponsem_pbRequest分别指代的响应和请求,这两个属性对应的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface GetUserRankLikeCountRequest : NSObject
@property(retain, nonatomic) NSString *appusername; // @dynamic appusername;
@property(retain, nonatomic) NSString *championname; // @dynamic championname;
@property(retain, nonatomic) NSString *rankid; // @dynamic rankid;
@end

@interface likeItem : NSObject
@property(nonatomic) unsigned int likecount; // @dynamic likecount;
@property(nonatomic) unsigned int likestate; // @dynamic likestate;
@property(nonatomic) unsigned int ranknum; // @dynamic ranknum;
@property(nonatomic) unsigned int score; // @dynamic score;
@property(retain, nonatomic) NSString *username; // @dynamic username;
@end

@interface GetUserRankLikeCountResponse : NSObject
@property(retain, nonatomic) NSMutableArray *follows; // 关注的好友排行信息 <Follow *>
@property(retain, nonatomic) NSMutableArray <likeItem *>*friendlikelist; // @dynamic friendlikelist;
@property(retain, nonatomic) NSMutableArray *likeuserlist; // @dynamic likeuserlist;
@property(retain, nonatomic) NSString *rankid; // @dynamic rankid;
@end

到此已经知道如何进行请求,以及获取到什么样的数据。接着hopper下DeviceRankSnsMgr的发送请求函数可以大致理出来微信发送请求的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)getUserRankListCount:(id)arg1 chanpionUsername:(id)arg2 brandUserName:(id)arg3{

GetUserRankLikeCountRequest * request = [[GetUserRankLikeCountRequest alloc] init];
[request setRankid:@"latestRank"];
[request setAppusername:@""];
[request setChampionname:@""];

ProtobufCGIWrap * cgiWrap = [[ProtobufCGIWrap alloc] init];
[cgiWrap setM_pbRequest:request];
[cgiWrap setM_uiCgi:0x412];

MMServiceCenter * serviceCenter = [MMServiceCenter defaultCenter];
EventService * eventService = [serviceCenter getService:[EventService class]];
unsigned int flag = [eventService CreateProtobufEvent:cgiWrap Flag:0x5];
if (flag != 0x0){
[CAppUtil addPBEventObserverListItem:flag andValue:self];
}
}

展示微信运动好友供选择:微信中选择好友/群/公众号等等信息的时候都是使用的ContactSelectView,它内部会通过ContactsDataLogic获取所有的好友/群/公众号信息,然后实现ContactSelectViewDelegate协议的代理执行一个- (_Bool)onFilterContactCandidate:(CContact *)arg1操作,对传入的CContact进行筛选展示。因此,不同的控制器可以使用ContactSelectView实现选择所有好友,选择所有的聊天群等功能。

5.非群主@所有人

在群组中,只有群主才有@全体成员的功能,现在要增加非群主也可以使用@全体成员的功能。群主@全体成员就是通过修改群公告,先研究群主修改完了公告之后的发消息流程。ChatRoomInfoEditDescViewController类就是修改群公告的控制器,在点击『完成』按钮之后出来alert提示,然后可以hook- (void)alertView:(id)arg1 clickedButtonAtIndex:(long long)arg2;函数,使用hopper进行Decopmile 这个函数发现里面进行了如下操作:

1
2
3
CGroupMgr * groupMgr = [[MMServiceCenter defaultCenter] getService:[CGroupMgr class]];
NSString * usrName = self.m_chatRoomContact.m_nsUsrName;
[groupMgr SetChatRoomDesc:usrName Desc:@"公告内容" Flag:0x1];

去模拟一下这个操作,失败,究其原因应该是,只有群主的id才有权限去发送成功这个消息,并且一旦发送成功(以群主身份),群公告也就会改变,这样也就只能作罢了。

换个思路,根据接收到的公告消息,分析与普通消息的之间的差异,去模拟发送这个消息。去hook接收到的群主@所有人的消息的时候发现(CMessageWrap *)wrapm_nsMsgSource 属性内容为如下(这个属性是微信用来对聊天消息进行的一层xml包裹):

1
2
3
4
5
6
7
8
<msgsource>
<sequence_id>
656201926
</sequence_id>
<atuserlist>
announcement@all
</atuserlist>
</msgsource>

需要sequence_id,记录操作顺序的id,另外一个很明显的是『announcement -> 群公告』,这个路看来只能再次作罢。

换一个研究方向,个人也是可以@个人的。这时候就是普通的发消息,但是其消息wrap.m_nsMsgSource

1
2
3
4
5
<msgsource>
<atuserlist>
***微信内部使用的用户id***
</atuserlist>
</msgsource>

如果放入一个id就可以完成艾特该id的用户,那么是不是可以将当前群里面的所有人的内部用户id都放到这里面,达到@所有人的目的呢?

实验之后,可以的。

所以,下面要做的就是,获取当前群里面所有用户的内部用户id,拼成字符串,放入这个属性的xml中,而这个内部使用的用户idCContact类的m_nsUsrName属性:

1
2
3
4
5
6
7
8
NSArray *result = [%c(CContact) getChatRoomMemberWithoutMyself:knToUsr];
NSMutableString *string = [NSMutableString string];
[result enumerateObjectsUsingBlock:^(CContact * obj, NSUInteger idx, BOOL * _Nonnull stop) {
[string appendFormat:@",%@",[obj m_nsUsrName]];
}];
NSString *sourceString = [string substringFromIndex:1];

[@"<msgsource><atuserlist>%@</atuserlist></msgsource>" , sourceString];

6.消息预览

在公众号内部的网页浏览信息、看朋友圈的时候来了信息,需要退返回主页面,然后如果要回到公众号、朋友圈的位置就要一步步的返回回去。因此,对要跳转到聊天室的时候做一个记录浏览的记录,就可以在消息和网页浏览之间切换了。

需要hook到收到聊天消息的方法,自然是哪哪儿都有他的-[CMessageMgr AsyncOnAddMsg:MsgWrap:]了,然后在这里记录消息内容,使用通知的方法将事件广播出去。在需要的界面通过添加通知,对通知中的内容进行展示,提供一个视图,点击的时候可以调抓到对应的聊天室,跳转之前需要记录当前控制器的调用栈。

公众号阅读控制器为:MMWebViewController,朋友圈控制器为: WCTimeLineViewController,所以可以hook这两个控制器的-viewDidLoad方法,添加通知。

更加极端的是为所有控制器的父类MMUIViewController添加通知,但是在聊天室控制器BaseMsgContentViewController中移除通知,这样就可以达到为所有非聊天室界面提供消息预览的功能了。

这个思路是参考这篇博文理出来的,我只不过是改进了一些东西。但是这个思路中有一个bug:从公众号网页点击消息预览回到聊天室,再从聊天室回到公众号网页,然后公众号网页一步步pop到根视图控制器,再进入聊天室点击回到公众号网页按钮,会发现整个网页什么都没有,而且公众号聊天界面是黑的。目前还没有想出来解决办法。

另外,在破乎上微信相关的问题下面,有人建议阅读的时候,可以半屏幕显示聊天框,以实现一边看一边聊,这么不伦不类的功能,我觉得微信是不会做的。虽然在阅读公众号的时候来了新信息需要退出是很多用户的痛点,但基于聊天和公众号之间的本质区别,最多会加一个跳转功能,不可能一边阅读一边聊天。

至于这么有强烈需求的功能,微信的团队从开始做公众号、企业号开始就没有考虑到么?基于聊天和阅读的本质区别,以及在微信中所扮演的角色有没有不同的权重值?可以参考一下👇产品经理们的理解。

参考文章:我是如何利用Xcode调试开发微信消息预览插件的

人人都是产品经理:想从微信文章快速跳到聊天,并没有你想象的那么容易

人人都是产品经理:阅读时回复、快速搜索收藏、消息管理优化,这是我给微信做的3个交互改善意见

7.自动回复

自动回复说白了,就是本地保存一个列表,列表中的每一个匹配规则类似于一个key-value,key是进行匹配的规则,使用正则匹配,value是匹配成功之后的回复消息。然后hook-[CMessageMgr AsyncOnAddMsg:MsgWrap:]判断接收到的消息内容去匹配本地列表,匹配成功了就返回对应要自动回复的内容。进行匹配的时候一定要有尽量多的判断条件,否则会引起自己发给自己然后循环往复的自动回复的bug。

为了避免上面提到的bug发生,除了在hook接收消息的时候严谨的判断,在设置匹配规则的时候也要尽可能的避免.*这种任何文本都可以匹配的正则表达式。另外为了防止误操作,仅支持单聊的时候对方发给自己消息的时候进行匹配,启动自动回复。

8.调试工具

在看wiki的时候发现支持pods,例子就是调试工具FLEX,本来是打算使用系统提供的调试工具来实现调试功能的,但是,很明显,在release模式下,调试工具是没有用的,一直显示不出来。

1
2
3
4
// http://ryanipete.com/blog/ios/swift/objective-c/uidebugginginformationoverlay/

Class debugCls = NSClassFromString(@"UIDebuggingInformationOverlay");
[debugCls performSelector:NSSelectorFromString(@"prepareDebuggingOverlay")];

按照wiki中的办法使用cocoaPods进行添加FLEX到项目中,然后就可以在微信中使用FLEX进行调试,很方便,并且还是开源的工具。

9.为聊天面板增加插件

插件面板、输入框、表情等等都是加在SelectAttachmentViewController控制器中的,而SelectAttachmentViewController的view加到MMInputToolView上供显示、操作的,他们之间的事件回调使用的是SelectAttachmentViewControllerDelegate协议。MMInputToolView是加在聊天室控制器中的视图,他们之间的事件回调是使用的MMInputToolViewDelegate协议。

SelectAttachmentViewController_arrAttachementObjectItems变量是用来存储插件对象的,存储的对象为AttachementObjectItem类,这个对象中有目前我们自定义一个插件需要的所有:图片、文字、点击回调SEL:

1
2
3
4
5
@interface AttachementObjectItem : NSObject
@property(retain, nonatomic) NSString *nsTitle; // @synthesize nsTitle=_nsTitle;
@property(retain, nonatomic) UIImage *oImage; // @synthesize oImage=_oImage;
@property(nonatomic) SEL selAction; // @synthesize selAction=_selAction;
@end

结合以上可以理一下插件的事件响应顺序为:

  • _arrAttachementObjectItems数组添加的插件类中的selAction是插件在点击的时候
  • 去询问控制器(SelectAttachmentViewController)的代理 (MMInputToolView)
  • 然后代理(MMInputToolView)实现协议(SelectAttachmentViewControllerDelegate)中的方法
  • 而且代理(MMInputToolView)自己也有他的代理,也就是聊天室控制器(BaseMsgContentViewController)
  • MMInputToolView再将事件传递给聊天控制器去实现插件中的操作,到此就完成了一个插件的事件调用链

查看头文件发现SelectAttachmentViewController是在-initObjectItem方法中初始化添加插件对象到_arrAttachementObjectItems中的,所以,就从hook这个函数开始按照上面的逻辑添加插件。

那么下面就简单了,按照上面的逻辑

  1. hook -initObjectItem函数为_arrAttachementObjectItems数组添加一个自定义的插件对象
  2. MMInputToolView添加一个 自定义插件中的需要响应的方法
  3. BaseMsgContentViewController添加一个MMInputToolView要传递过来的插件响应方法的方法

完成以上之后发现,群聊和单聊都添加了一个插件,很明显这不是想要的结果,需要想办法不为单聊添加自定义插件。那么对比一下群聊和单聊插件面板的差别可以发现:群聊可以群视频,群聊不可以转账,单聊只可以单个视频。使用FLEX查找发现不同的面板中有好几个属性是不一样的,不过其中一个@property(nonatomic) _Bool allowMultiTalk;属性的不同是很能区别出来是群聊还是单聊。实验一下,确实可以区分出来。

通过这个插件,可以为上面的@所有人功能在聊天室内添加更便捷的操作入口。

10.表情相关

表情相关是直接使用的这个仓库,里面有可以直接从web页面将图片(静态图、gif图)保存为表情,并且修改了表情最大限制尺寸。

Github:一键保存为表情,无视微信表情大小的限制

11.将已发送的文字朋友圈在私密/公开之间切换

朋友圈的图片可以自由的进行『私密』->『公开』切换,但是文字却没有这个功能,一旦在是发送文字朋友圈的时候选择隐私,那么就一直是隐私,不可以再改为公开,相反,选择公开,就不可以再改为隐私。如果图片版可以自由切换,那么同为朋友圈的文字版应该也可以进行切换的。

首先,要找到图片版和文字版的区别和共同点。使用FLEX发现,图片和文字都有使用的WCDataItem来表示一条,不过由于图片版可能会有多张图片,所以他又包了一层,使用WCMediaItemWrap来表示,具体需要的东西如下:

1
2
3
4
5
6
7
8
9
10
@interface WCDataItem : NSObject
@property(retain, nonatomic) NSString *username;
@property(nonatomic) _Bool isPrivate;
@end

@interface WCMediaItemWrap : NSObject
@property(nonatomic) unsigned int index;
@property(retain, nonatomic) WCDataItem *parent;
@property(retain, nonatomic) WCMediaItem *mediaItem;
@end

WCPhotoMutipleImageViewController是图片版本的控制器,里面的actionSheet回调里面有进行『私密/公开』的操作,hopper他的- (void)actionSheet:(WCActionSheet *)arg1 clickedButtonAtIndex:(long long)arg2;方法会发现,分别调用如下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 设为公开
- (void)onMakePublic
{
...
WCMediaItemWrap * mediaItemWrap = [self getMediaItemWrapAnywayAt:self.m_iCurrentPage];
WCFacade * facade = [[MMServiceCenter defaultCenter] getService:[WCFacade class]];
WCDataItem * parent = mediaItemWrap.parent;
[facade setDataItemPublic:parent];
...
}

// 设为隐私
- (void)onMakePrivate
{
...
WCMediaItemWrap * mediaItemWrap = [self getMediaItemWrapAnywayAt:self.m_iCurrentPage];
WCFacade * facade = [[MMServiceCenter defaultCenter] getService:[WCFacade class]];
WCDataItem * parent = mediaItemWrap.parent;
[facade setDataItemPrivate:parent];
...
}

重点就是里面的WCFacade类的-setDataItemPrivate:(WCDataItem *)arg1-setDataItemPublic:(WCDataItem *)arg1这两个方法,而文字版本就是使用WCDataItem来表示的,所以,按道理传入文字版的WCDataItem实例就可以进行私密/公开的切换了。

结合以上WCFacade头文件中有价值的信息为:

1
2
3
4
@interface WCFacade : NSObject
- (void)setDataItemPublic:(WCDataItem *)arg1;
- (void)setDataItemPrivate:(WCDataItem *)arg1;
@end

接下来就可以在文字朋友圈详情界面进行添加操作入口。WCCommentDetailViewControllerFB就是文字版本详情控制器,他有一个属性@property(retain, nonatomic) WCDataItem *dataItem;代表当前的朋友圈数据,结合图片朋友圈隐私/公开的操作逻辑,可以使用这个属性来做私密/公开操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 文字朋友圈设为公开
- (void)_textDetailOnMakePublic
{
//[self startLoadingBlocked];
WCFacade * facade = [[MMServiceCenter defaultCenter] getService:[WCFacade class]];
[facade setDataItemPublic:self.dataItem];
}

// 文字朋友圈设为隐私
- (void)_textDetailOnMakePrivate
{
//[self startLoadingBlocked];
WCFacade * facade = [[MMServiceCenter defaultCenter] getService:[WCFacade class]];
[facade setDataItemPrivate:self.dataItem];
}

添加以上的方法,经测试,发现是可以把文字版本朋友圈进行私密/公开的转换。

12.聊天机器人

自动回复的升级版,todo。

思路是使用pods添加图灵机器人的SDK,然后进行hook接收消息进行匹配进行对应的消息回复,不知道这么做会不会被微信封号,毕竟这是一个连着崩溃三次就会进入安全模式的App。

CSDN:使用python基于web版如何制作机器人

Github:wxBot

Github:itChat

Github:wxpy

图灵机器人

13.一键开启/屏蔽【群、好友】消息

这个是在调试的时候想到的一个功能,因为要不断的切换屏蔽/开启来调试消息预览,然而开关又比较深:需要进入聊天详情,转换开关。聊天室内头部的聊天信息上有是否开启屏蔽的标志,是否可以将转换屏蔽的操作放到这个聊天信息上呢?期望的功能也不需要提示,如果屏蔽,点击立即开启,如果开启,点击立即屏蔽,没有任何复杂操作,最主要是不需要离开聊天室。微信将这个功能放到详情里面,是不是有自己深层次的考虑呢,还是没有想到呢,暂且不提这些,先分析一下这个功能如何实现。

单聊聊天室详情界面是AddContactToChatRoomViewController控制器,查看头文件通过关键字发现-setUpdateNotifyMuted:方法应该是用来控制是否开启屏蔽的,在转换的时候hook一下发现,的确是这个函数内部执行的操作,然后hopper一下整理出来内部实现:

1
2
3
4
5
6
7
8
- (id)setUpdateNotifyMuted:(id)arg2 {
[self.m_contact setChatStatusNotifyOpen:NO];
if (nil != self.m_delaySwitchLogic){
NSString * friendName = self.m_chatRoomContact.m_nsUsrName;
BOOL isOpen = self.m_chatRoomContact.isChatStatusNotifyOpen;
[self.m_delaySwitchLogic chatProfileSwitchSetting:chatRoomName withType:0x2 andValue:!isOpen];
}
}

其中m_delaySwitchLogic属性是DelaySwitchSettingLogic类直接alloc-init初始化的,因此如果在其他地方可以模拟这个操作了。另外对DelaySwitchSettingLogic也进行了hopper,发现里面的逻辑还是很复杂的。

群聊聊天室详情界面是ChatRoomInfoViewController控制器,其内部头文件中的- (void)setMuteStatus:(id)arg1;- (_Bool)setUpdateNotifyMuted:(_Bool)arg1两个方法应该是设置屏蔽的操作,hook一下发现后者内部完成了改变逻辑,然后使用hopper整理出来内部实现:

1
2
3
4
5
6
7
8
-setUpdateNotifyMuted:{

if (nil != self.m_delaySwitchLogic){ // DelaySwitchSettingLogic
NSString * chatRoomName = self.m_chatRoomContact.m_nsUsrName;
BOOL isOpen = self.m_chatRoomContact.isChatStatusNotifyOpen;
[self.m_delaySwitchLogic chatProfileSwitchSetting:chatRoomName withType:0x2 andValue:!isOpen];
}
}

和单聊的m_delaySwitchLogic一样。

知道了以上的逻辑,下面就可以为聊天室的标题视图self.navigationItem.titleView添加手势,完成点击切换屏蔽消息的操作。但是在实际软件中titleView的frame很小,其子视图倒是很大,具体的结构如下

子视图的显示区域已经超出了父视图,只能重写MMTitleView-hitTest:withEvent:事件了。由于手势是加在了红色的视图上,因此在点击的时候会有一些小瑕疵,需要尽量的往右边一点。。。

14.群主一键删除成员

群主删除成员需要三步,跳转两个界面:点击右上角进入群详情界面,点击删除进入选择群成员界面,挑选要删除的成员,然后完成删除。改进一下,不进行界面跳转,直接在聊天室内@要删除的成员,然后删除。并且这个功能不同于@所有人,只有群主才会拥有,因此在自定义聊天室插件的时候还要注意判断当前用户是不是群聊天的群主,参考SelectAttachmentViewController头文件可以可以添加一个属性@property(nonatomic) _Bool mm_currentContactIsAdmin;,因为这个类里面没有任何的关联聊天室身份的类,比如CContact,并且MMInputToolView中也没有,需要为他也添加一个属性。

先分析一下选择群成员,然后执行删除的操作是如何完成的。选择群成员的控制器是RoomContactSelectViewController,完成选择,点击右上角弹出来alert,然而头文件中没有alert的代理回调方法,难道是使用的UIAlertController?再查看一下,貌似有用的仅仅有- (void)OnDataChange;这一个方法,感觉也不对,没办法,那直接hopper吧。

发现内部有一个方法内部有执行成员管理的操作:

1
2
3
4
5
6
7
8
9
- (void)onDeleteMember:(NSArray *)arg2 {

if (self.m_roomContact.isAdmin){

[self startLoadingNonBlock];
CGroupMgr * groupMgr = [MMServiceCenter defaultCenter] getService:[CGroupMgr class]];
[groupMgr DeleteGroupMember:chatRoomUsrName withMemberList:[NSArray arrayWithObjects:arg2] scene:0];
}
}

方法内部使用了群组管理类CGroupMgr,hook一下他的- (_Bool)DeleteGroupMember:(id)arg1 withMemberList:(id)arg2 scene:(unsigned long long)arg3;函数发现,第一个参数是要删除的聊天室id,第二个参数是数组,里面是要删除的成员id,第三个参数是flag 为 0。

接下来需要获取已经艾特到的用户。需要获取到输入框中 @ 到的成员,长按成员头像可以将该用户放入输入框,查询头文件发现- (void)longPressOnHeadImage:(id)arg1;是调用的这个方法,使用hopper查看内部实现,将@到的用户交给m_delegate-addAtUser:方法,为RoomContentLogicController,可以在这个长按方法里面用数组进行记录@到的用户,然后使用插件直接删除数组中的成员。但是有一个问题,输入框中是可以删除已有的@成员,也可以根据输入中含有@字符跳转界面供选择要艾特的成员,这样对数组的操作就需要考虑很多东西,比较麻烦。

输入框中输入@字符会跳转到选择提醒的好友列表,该列表是RoomContactSelectViewController控制器,在点击对应的成员的时候,通过协议RoomContactSelectDelegate中的- (void)didSelectContact:(CBaseContact *)arg1;回调告诉代理选中了某一个成员,他的代理为RoomContentLogicController,而前面在长按用户头像也是将用户交给的这个类,那么hopper他看一下内部是做了何种操作:

1
2
3
4
5
- (void)AddAtUser:(CContact *)arg2 
{
MMNewSessionMgr * newSesMgr = [[MMServiceCenter defaultCenter]getService:[MMNewSessionMgr class]];
[newSesMgr addContact:self.m_contact AtUser:arg2.m_usrName];
}

接下来查询MMNewSessionMgr头文件发现,可以通过聊天室id获取一个sessionInfo

1
2
3
4
5
@interface MMNewSessionMgr : NSObject{
NSMutableArray *m_arrSession;
}
- (id)addContact:(CContact *)arg2 AtUser:(NSString *)usrName;
- (MMSessionInfo *)GetSessionByUserName:(NSString *)chatRoomUsrName;

MMSessionInfo类的内部有一个属性,在进行艾特好友的时候会变化,

1
2
3
@interface MMSessionInfo : NSObject
@property(retain, nonatomic) NSString *m_atUserList; // @synthesize m_atUserList;
@end

虽然通过这个属性可以拿到已经艾特的成员,但是如果如果输入框中将已经艾特的用户删除,这个属性中并没有删除。看来还是需要再找找了。

到这里看不下4个类之间的回调关系以及他们的汇编代码,我都快疯了。偷懒的话,可以直接在点击删除的时候去遍历输入框内的文本,使用正则找出来已经艾特的群成员(成员的昵称),然后去获取这些群成员的id,执行上面的删除操作。仅仅是取巧的办法,没有从根本上实现功能,有可能会有bug。

15.一键为微信运动好友点赞

有一个以前不知道因为什么原因加的好友,微信运动每天晚上8点多给我点赞,连着几个星期之后,我觉得他是在做一个类似于打卡的行为:每天坚持运动,完了为微信运动中所有的好友点赞,打卡完成。既然用户有这样的需求,就可以试着把这个功能加入到软件中。

先分析一下为一个好友点赞的操作。排行榜控制器类为BraceletRankViewController,头文件中的-(void)onClickLike:(id)arg1;方法看起来是点击红心的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- onClickLike:(BraceletRankLikeButton *)arg{
DeviceRankInfo * rankInfo = arg.m_rankInfo;
if(is_click_self){
// 跳转到为自己点赞的列表
}else{
if (0 != rankInfo.score && rankInfo.hasLike){// 已经点过赞了的
BOOL contains = [self.friendLikeSet containsObject:rankInfo];
NSUinterger likeCount = rankInfo.likeCount;
if (contains){
//likeCount
[self.friendLikeSet removeObject:rankInfo];
[self animatChangeLikeStateForUser: itToLikeState:NO];
}
}else{// 还没有点赞
[self.friendLikeSet addObject:rankInfo];
[self animatChangeLikeStateForUser: itToLikeState:YES];
}
}
}

这个函数内部通过判断已经点赞的话就从一个集合(friendLikeSet)中移除,没有点赞就加入集合,然后调了一个函数-animatChangeLikeStateForUser:isTolikeState:,通过函数名字可以猜出来,做数据上传的可能性不是很大,只是根据参数做了一个视图动画。现在知道了,被点赞的好友会被放入一个集合中,通过FLEX查看到集合中的数据是以DeviceRankInfo类的实例来表示的,涉及到的类如下:

1
2
3
4
5
6
7
8
@interface DeviceRankInfo : NSObject
@property(nonatomic) _Bool hasLike; // @synthesize hasLike=_hasLike;
@property(retain, nonatomic) NSString *username; // @synthesize username=_username;
@end

@interface BraceletRankLikeButton : MMUIButton
@property(retain) DeviceRankInfo *m_rankInfo; // @synthesize m_rankInfo;
@end

全局搜索friendLikeSet发现,在视图消失的时候有进行数据的上传:

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidDisappear:(id)arg2 {

if(self.friendLikeSet != nil){
NSUInterger count = self.friendLikeSet.count;
if (count){
...
DeviceRankSnsMgr * rankSnsMgr = [[MMServiceCenter defaultCenter]getService:[DeviceRankSnsMgr class]];
[rankSnsMgr likeFriendRank:self._rankId FriendUsernames:@"" brandUserName:@"" optype:0x1];
}
}
}

里面通过服务中心中的DeviceRankSnsMgr调用了-likeFriendRank:FriendUsernames:brandUserName:optype:函数,hook一下该函数发现参数为:

1
2
3
4
5
6
7
8
@interface DeviceRankSnsMgr : MMService
- (void)likeFriendRank:(NSStirng *)arg1 FriendUsernames:(NSArray *)arg2 brandUserName:(NSString *)arg3 optype:(unsigned int)arg4;
@end

第一个参数为 BraceletRankViewController 的 _rankId 属性,
第二个参数为要点赞的好友数组,内部装的是用户id
第三个传空字符串
第四个传1

因此,只需要获取到所有排行榜中好友的id放入数组,使用DeviceRankSnsMgr的这个方法就可以为所有的好友进行点赞。

16.为新添加好友发送问候语

一个很实用,同时也是很有趣的功能。一些特殊人群,需要添加大量的好友,比如销售、推广之类的用户,加了别人第一件事就是介绍自己,加深第一印象,每次都是一样的文案,每次都要复制粘贴过来,会显得很机械。当然不限于需要自我介绍这种情况,为新朋友发送一条暖心的问候语也还是很有意思的。

以下添加好友仅仅测试了使用二维码和雷达加好友,至于搜索、聊天群里添加等等应该都是一样的,毕竟归根结底都是添加好友这个操作

首先需要探究一下,当添加了一个用户之后的界面显示特点。陌生人通过扫描二维码点击添加到通讯录之后,本机账号这边会有一个异步消息,通过hook下CMessageMgr-AsyncOnAddMsg:MsgWrap:可以发现

1
wrap.m_uiMessageType = 10000;

多次试验之后确定添加好友的类型就是10000,因此可以根据这个信息来判断是否是刚添加的好友。

接着分析添加好友的场景:自己添加别人别人添加自己

16.1 别人添加自己为好友

当别人通过二维码添加自己到通讯录的时候,本地会发送一个消息『***刚刚把你添加到通讯录,现在可以开始聊天了』,这个消息类型是10000,是他的 m_uiMesLocalID = 1

除了这个消息提示,界面上还会出现一个添加好友的按钮,当点击了添加之后,会再次发送一个消息,这个消息也是10000类型的,但是他的 m_uiMesLocalID = 2,然后本地发送一个『你已经添加了***,现在可以开始聊天了』

通过上面的流程是不是可以理解为:

微信添加好友的机制的是对方添加自己之后,自己却没有添加对方,还需要自己再添加一下对方才可以。

而且 m_uiMesLocalID 表示的是和该用户的本地消息记录id,从1开始,所以后面自己再点击添加按钮之后会变成2。

结合m_uiMesLocalIDm_uiMessageType这两个属性,可以判断出来是不是新添加的好友。

16.2 自己添加别人为好友

当扫描别人的二维码之后的消息类型和上面的类似只不过是发送和接收方的不同,m_uiMesLocalIDm_uiMessageType这两个属性和上面分析的一样。

这里有一个问题,在我多次添加/删除好友之后,添加那一方不会再收到上面提到的消息,而被添加方还是可以收到上面的消息。不知道微信内部做了什么。所以,现在的做法会有一个不定时的bug:自己添加别人为好友的时候可能不会触发判断条件,不能发送问候语。后来看了一下,应该是短时间内添加/删除太频繁导致的。

17.复现被好友删除的评论

一个场景是这样的,你评论了好友的一个圈文,然后他回复了你,但是你没有即时看到,过了一会儿他又回复了一句,但是把前一句删除了,留给你的只是这样的一个界面,这不是要逼死人么?如果删了就不要告诉我删了,就跟撤回消息是一个道理,撤了就不要告诉我了,好么。

牢骚就少发吧,探讨一下如何复现被删除的评论。使用FLEX观察界面会发现,删除的和没有删除的都是一样的数据结构,为WCUserComment,他内部是使用content属性来表示评论内容的,具体的类内部结构如下:

1
2
3
4
5
6
7
8
@interface WCUserComment : NSObject <NSCoding>
@property(nonatomic) int isRichText; // @synthesize isRichText;
@property(retain, nonatomic) NSString *contentPattern; // @synthesize contentPattern;
@property(nonatomic) int type; // @synthesize type;
@property(retain, nonatomic) NSString *content; // @synthesize content;
@property(retain, nonatomic) NSString *nickname; // @synthesize nickname;
@property(retain, nonatomic) NSString *username; // @synthesize username;
@end

不过他是作为WCSNSMessagecomment属性来供控制器使用的,类具体内部如下:

1
2
3
4
5
6
7
8
9
@interface WCSNSMessage : NSObject
@property(retain, nonatomic) WCSNSRewardInfo *rewardInfo; // @synthesize rewardInfo;
@property(nonatomic) unsigned int delStatus; // @synthesize delStatus;
@property(retain, nonatomic) WCUserComment *refComment; // @synthesize refComment;
@property(retain, nonatomic) WCUserComment *comment; // @synthesize comment;
@property(retain, nonatomic) NSString *parentObjID; // @synthesize parentObjID;
@property(retain, nonatomic) NSString *objID; // @synthesize objID;
@property(retain, nonatomic) NSString *msgID; // @synthesize msgID;
@end

评论界面控制器的类为WCCommentListViewController,而作为包裹评论的WCSNSMessage类中有一个属性是很明显是用来区别是否被删除还是没有被删除—unsigned int delStatus。ok,目前为止可以在评论列表reloadData的时候进行获取被删除的评论。

在做这个的时候,发现评论列表入口只会在有评论的时候才会出现,除了这个之外,再也找不到其他的入口。对比其他的具有评论功能的社交App来看,这样的操作还是很少见的,应该说是就微信一家:

  • 微博的当前版本(7.10.3)是有一个专门的通知来记录所有的和自己相关的评论信息
  • 手Q内部的qq空间是有一个单独的消息tab可以进入消息列表
  • 网易云音乐内部的消息模块中也有专门展示个人状态的评论栏
  • 其他的社交软件基本上都不使用了,不过记忆中都是有可以看所有评论信息的入口的

为什么微信要这么做呢?我个人感觉,微信认为朋友圈的内容存活时间不会太长,基本上2-3天,这段时间之后就会被新的内容所淹没在下一页中,而点赞和评论也就会随着内容的逐渐退出当日热点而隐退。不为用户展示评论列表入口,第一是为了提高新内容的曝光率,降低旧闻的出境几率;第二是不打扰用户,只在有的时候才给你展示,没有的时候你不需要操心这个可有可无的东西,因为在当前的那个圈文下面是可以看到评论和点赞内容的,当有新的评论内容的时候提供一个入口是为了让消息有一个即时性。不过,如果用户进入消息列表之后,还没来得及对评论做回复,就因为各种以外的原因退出了这个界面或者直接退出了微信,那么再进入这个界面就不可能了,只能等新的消息到来,退一步可选的就是往下滑,滑到当前的那一天圈文的位置。因此,当初想加一个进入评论列表的入口,在最后还是按照微信原有流程来,没有加。

18.在聊天室内部增加滚动截屏,截取聊天记录

19.朋友圈生成年度报告

20.导出微信聊天记录


以上这些功能希望能有助于看官更加舒服的使用微信。

能看到这里的,估计都是动手能力很强的,那么何尝不开始使用MonkeyDev来客制化一个属于自己的微信呢。