让微信 macOS 客户端支持 Alfred
Alfred 作为 macOS 平台上的效率神器,Dash、Evernote等应用配合 Alfred 能够有很好的效率提升与体验,然而还是有很多应用未能支持得,比如 Telegram、微信等经常使用的一些应用。既然原生未能支持,那么自己来动手搞一个了~
0x00 相关回顾
以上为微信 Tweak 增强的以往的一些文章资料,可供参考。
0x01 需求
- 通过 Alfred 模糊搜索联系人并快速跳转到对应聊天界面
- 还没想到 (:з」∠)
0x02 Cocoa App 与 Alfred 的通讯
通过 Google 粗略地搜索了一遍,发现 Cocoa App 适配 Alfred 的相关开发教程并没有很多,大概有如下两种方式进行通讯:
1. AppleScript
对于 AppleScript,在 macOS 平台上是随处可见的了,文档 ScriptableCocoaApplications 中也有详细的介绍与教程。但由于需要编写 sdef(Scripting Definition File)
文件让我有点头疼,因此本文选择的方式是后者的方法。
2. 内置 HTTP Server
内置 HTTP Server 也就是采用 HTTP GET/POST 的方式进行调用通讯,没有繁琐的配置项,也不用修改 Info.plist
文件,自由度相对较高,不受限于 AppleScript 的调用。因此选用了 GCDWebServer 作为 HTTP Server。
0x03 搜索方案
确定了 Cocoa Application 与 Alfred 的通讯方案之后,那么就需要考虑微信客户端的搜索。第一时间想到的应该是直观界面上的搜索框,通过动态调试找出搜索的方法获得结果。比较底层的方法则是通过获取所有联系人列表的方法,假如能够自定义筛选参数那就更好了。因此动态调试有以下两个关注点:
- 搜索栏事件响应与调用
- 联系人列表获取方法
继续祭出 Hopper Disassembler
、class-dump
等工具。
0x04 找出入口
首先采用的是第一种搜索方案,通过观察,可以定位到搜索的主要方法在 MMSearchViewController
中,通过调用 - (void)searchWithKeyword:(id)keyword andShowInView:(id)view;
方法无需自己实现模糊匹配即可得到搜索结果MMSearchResultItem
的数组,但是有一点需要注意的是 view
的传值不能为空,可以说是非常方便了。然而际上回调与传值都不方便使用,其中还有 NSTimer
的处理操作……
于是转战第二种搜索方案,经过以往的微信客户端逆向实践,不难找出 ContactStorage
与 WCContactData
这两个与联系人相关的 Header,其中 ContactStorage
更是清楚列出了与联系人操作的相关方法,比如 - (id)GetAllFriendContacts;
。但是这个方案就需要自己实现模糊搜索过滤的操作啦~
PS: 其中有一个方法 - (void)SearchContactWithKeyword:(id)arg1;
名字非常美好,然而内部并没有任何实现,只有一个 return
,大家不要被骗了!(♯`∧´)
0x05 编写 Tweak
获得以上主要方法之后,就可以进行 Tweak 的实现。
1. HTTP Server
HTTP Server 的实现可以通过单例实现,从应用启动开始一直到应用生命周期结束,也就是被 Terminate。
#import "AlfredManager.h"
#import "WeChatTweakHeaders.h"
@interface AlfredManager()
@property (nonatomic, strong, nullable) GCDWebServer *server;
@end
@implementation AlfredManager
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static AlfredManager *shared;
dispatch_once(&onceToken, ^{
shared = [[AlfredManager alloc] init];
});
return shared;
}
- (void)startListener {
if (self.server != nil) {
return;
}
self.server = [[GCDWebServer alloc] init];
// Search contancts
[self.server addHandlerForMethod:@"GET" path:@"/wechat/search" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse * _Nullable(__kindof GCDWebServerRequest * _Nonnull request) {
// Handle search request
}];
// Start chat
[self.server addHandlerForMethod:@"GET" path:@"/wechat/start" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse * _Nullable(__kindof GCDWebServerRequest * _Nonnull request) {
// Handle start chat request
}];
[self.server startWithOptions:@{GCDWebServerOption_Port: @(48065),
GCDWebServerOption_BindToLocalhost: @(YES)} error:nil];
}
- (void)stopListener {
if (self.server == nil) {
return;
}
[self.server stop];
[self.server removeAllHandlers];
self.server = nil;
}
从上面代码可以看到,server
绑定了两个 API,分别是 /wechat/search
与 /wechat/start
, 分别对应着搜索联系人和打开聊天窗口。
2. 模糊搜索联系人
NSString *keyword = [request.query[@"keyword"] lowercaseString] ? : @"";
NSArray<WCContactData *> *contacts = ({
MMServiceCenter *serviceCenter = [objc_getClass("MMServiceCenter") defaultCenter];
ContactStorage *contactStorage = [serviceCenter getService:objc_getClass("ContactStorage")];
GroupStorage *groupStorage = [serviceCenter getService:objc_getClass("GroupStorage")];
NSMutableArray<WCContactData *> *array = [NSMutableArray array];
[array addObjectsFromArray:[contactStorage GetAllFriendContacts]];
[array addObjectsFromArray:[groupStorage GetAllGroups]];
array;
});
NSArray<WCContactData *> *results = ({
NSMutableArray<WCContactData *> *results = [NSMutableArray array];
for (WCContactData *contact in contacts) {
BOOL isFriend = contact.m_uiBrandSubscriptionSettings == 0;
BOOL containsNickName = [contact.m_nsNickName.lowercaseString containsString:keyword];
BOOL containsUsername = [contact.m_nsUsrName.lowercaseString containsString:keyword];
BOOL containsAliasName = [contact.m_nsAliasName.lowercaseString containsString:keyword];
BOOL containsRemark = [contact.m_nsRemark.lowercaseString containsString:keyword];
BOOL containsNickNamePinyin = [contact.m_nsFullPY.lowercaseString containsString:keyword];
BOOL containsRemarkPinyin = [contact.m_nsRemarkPYFull.lowercaseString containsString:keyword];
BOOL matchRemarkShortPinyin = [contact.m_nsRemarkPYShort.lowercaseString isEqualToString:keyword];
if (isFriend && (containsNickName || containsUsername || containsAliasName || containsRemark || containsNickNamePinyin || containsRemarkPinyin || matchRemarkShortPinyin)) {
[results addObject:contact];
}
}
results;
});
3. 打开聊天窗口
WCContactData *contact = ({
NSString *session = request.query[@"session"];
WCContactData *contact = nil;
if (session != nil) {
MMServiceCenter *serviceCenter = [objc_getClass("MMServiceCenter") defaultCenter];
if ([session rangeOfString:@"@chatroom"].location == NSNotFound) {
ContactStorage *contactStorage = [serviceCenter getService:objc_getClass("ContactStorage")];
contact = [contactStorage GetContact:session];
} else {
GroupStorage *groupStorage = [serviceCenter getService:objc_getClass("GroupStorage")];
contact = [groupStorage GetGroupContact:session];
}
}
contact;
});
dispatch_async(dispatch_get_main_queue(), ^{
[[objc_getClass("WeChat") sharedInstance] startANewChatWithContact:contact];
[[objc_getClass("WeChat") sharedInstance] showMainWindow];
[[NSApplication sharedApplication] activateIgnoringOtherApps: YES];
});
4. 测试接口
当完成了以上步骤,可以通过浏览器测试接口:
0x06 编写 Workflow
当完成了以上步骤,我已经不想动了 (:з」∠)
Alfred workflow 的编写可以使用各种个样的语言,比如宇宙第一的 PHP。而我选择了人生苦短的 Python。首先肯定上 GitHub 淘个库来用了:alfred-workflow。
由于目前接口比较单一,因此主要文件只有两个:
search.py
:负责联系人的搜索def search(query): url = 'http://localhost:48065/wechat/search' parameters = dict(keyword=query) response = web.get(url, parameters) response.raise_for_status() return response.json() def main(wf): query = wf.args[0] for contact in search(query): title = None if 'm_nsRemark' in contact and len(contact['m_nsRemark']) > 0: title = contact['m_nsRemark'] else: title = contact['m_nsNickName'] wf.add_item(title=title, subtitle=contact['m_nsNickName'], arg=contact['m_nsUsrName'], valid=True) wf.send_feedback()
start.py
:负责打开微信跳转def start(query): url = 'http://localhost:48065/wechat/start' parameters = dict(session=query) response = web.get(url, parameters) response.raise_for_status() return response def main(wf): query = wf.args[0] start(query) wf.send_feedback()
0x07 最终效果
GitHub: WeChatTweak-macOS ,欢迎交流讨论。