io.uouo.wechat.api.WeChatApiImpl Maven / Gradle / Ivy
The newest version!
package io.uouo.wechat.api;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import io.uouo.wechat.WeChatBot;
import io.uouo.wechat.api.client.BotClient;
import io.uouo.wechat.api.constant.Constant;
import io.uouo.wechat.api.constant.StateCode;
import io.uouo.wechat.api.enums.AccountType;
import io.uouo.wechat.api.enums.ApiURL;
import io.uouo.wechat.api.enums.RetCode;
import io.uouo.wechat.api.model.*;
import io.uouo.wechat.api.request.BaseRequest;
import io.uouo.wechat.api.request.FileRequest;
import io.uouo.wechat.api.request.JsonRequest;
import io.uouo.wechat.api.request.StringRequest;
import io.uouo.wechat.api.response.*;
import io.uouo.wechat.exception.WeChatException;
import io.uouo.wechat.utils.*;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static io.uouo.wechat.api.constant.Constant.*;
/**
* 微信API实现
*
* @author biezhi
* @since 2018/1/21
*/
@Slf4j
public class WeChatApiImpl implements WeChatApi {
private static final Pattern UUID_PATTERN = Pattern.compile("window.QRLogin.code = (\\d+); window.QRLogin.uuid = \"(\\S+?)\";");
private static final Pattern CHECK_LOGIN_PATTERN = Pattern.compile("window.code=(\\d+)");
private static final Pattern PROCESS_LOGIN_PATTERN = Pattern.compile("window.redirect_uri=\"(\\S+)\";");
private static final Pattern SYNC_CHECK_PATTERN = Pattern.compile("window.synccheck=\\{retcode:\"(\\d+)\",selector:\"(\\d+)\"}");
private String uuid;
private boolean logging;
private int memberCount;
private WeChatBot bot;
private BotClient client;
/**
* 所有账号
*/
@Getter
private Map accountMap = new HashMap<>();
/**
* 特殊账号
*/
@Getter
private List specialUsersList = Collections.EMPTY_LIST;
/**
* 公众号、服务号
*/
@Getter
private List publicUsersList = Collections.EMPTY_LIST;
/**
* 好友列表
*/
@Getter
private List contactList = Collections.EMPTY_LIST;
/**
* 群组
*/
@Getter
private List groupList = Collections.EMPTY_LIST;
/**
* 群UserName列表
*/
private Set groupUserNames = new HashSet<>();
public WeChatApiImpl(WeChatBot bot) {
this.bot = bot;
this.client = bot.client();
}
/**
* 自动登录
*/
private void autoLogin() {
String file = bot.config().assetsDir() + "/login.json";
try {
HotReload hotReload = WeChatUtils.fromJson(new FileReader(file), HotReload.class);
hotReload.reLogin(bot);
} catch (FileNotFoundException e) {
this.login(false);
}
}
@Override
public void login(boolean autoLogin) {
if (bot.isRunning() || logging) {
log.warn("微信已经登录");
return;
}
if (autoLogin) {
this.autoLogin();
} else {
this.logging = true;
while (logging) {
this.uuid = pushLogin();
if (null == this.uuid) {
while (null == this.getUUID()) {
DateUtils.sleep(10);
}
log.info("开始下载二维码");
this.getQrImage(this.uuid, bot.config().showTerminal());
log.info("请使用手机扫描屏幕二维码");
}
Boolean isLoggedIn = false;
while (null == isLoggedIn || !isLoggedIn) {
String status = this.checkLogin(this.uuid);
if (StateCode.SUCCESS.equals(status)) {
isLoggedIn = true;
} else if ("201".equals(status)) {
if (null != isLoggedIn) {
log.info("请在手机上确认登录");
isLoggedIn = null;
}
} else if (!"408".equals(status)) {
break;
}
DateUtils.sleep(300);
}
if (null != isLoggedIn && isLoggedIn) {
break;
}
if (logging) {
log.info("登录超时,重新加载二维码");
}
}
}
this.webInit();
this.statusNotify();
this.loadContact(0);
log.info("应有 {} 个联系人,读取到联系人 {} 个", this.memberCount, this.accountMap.size());
System.out.println();
log.info("共有 {} 个群 | {} 个直接联系人 | {} 个特殊账号 | {} 公众号或服务号",
this.groupUserNames.size(), this.contactList.size(),
this.specialUsersList.size(), this.publicUsersList.size());
// 加载群聊信息,群成员
this.loadGroupList();
log.info("[{}] 登录成功.", bot.session().getNickName());
this.startRevive();
this.logging = false;
}
/**
* 获取UUID
*
* @return 返回uuid
*/
private String getUUID() {
log.info("获取二维码UUID");
// 登录
ApiResponse response = this.client.send(new StringRequest("https://login.weixin.qq.com/jslogin")
.add("appid", "wx782c26e4c19acffb")
.add("redirect_uri", "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop&fun")
.add("fun", "new")
.add("lang", "en_")
.add("_", System.currentTimeMillis())
);
Matcher matcher = UUID_PATTERN.matcher(response.getRawBody());
if (matcher.find() && StateCode.SUCCESS.equals(matcher.group(1))) {
this.uuid = matcher.group(2);
}
return this.uuid;
}
/**
* 读取二维码图片
*
* @param uuid 二维码uuid
* @param terminalShow 是否在终端显示输出
*/
private void getQrImage(String uuid, boolean terminalShow) {
String uid = null != uuid ? uuid : this.uuid;
String imgDir = bot.config().assetsDir();
FileResponse fileResponse = this.client.download(
new FileRequest(String.format("%s/qrcode/%s", Constant.BASE_URL, uid)));
InputStream inputStream = fileResponse.getInputStream();
File qrCode = WeChatUtils.saveFile(inputStream, imgDir, "qrcode.png");
DateUtils.sleep(200);
try {
QRCodeUtils.showQrCode(qrCode, terminalShow);
} catch (Exception e) {
this.getQrImage(uid, terminalShow);
}
}
/**
* 检查是否登录
*
* @param uuid 二维码uuid
* @return 返回登录状态码
*/
private String checkLogin(String uuid) {
String uid = null != uuid ? uuid : this.uuid;
String url = String.format("%s/cgi-bin/mmwebwx-bin/login", Constant.BASE_URL);
Long time = System.currentTimeMillis();
ApiResponse response = this.client.send(new StringRequest(url)
.add("loginicon", true).add("uuid", uid)
.add("tip", "1").add("_", time)
.add("r", (int) (-time / 1000) / 1579)
.timeout(30));
Matcher matcher = CHECK_LOGIN_PATTERN.matcher(response.getRawBody());
if (matcher.find()) {
if (StateCode.SUCCESS.equals(matcher.group(1))) {
if (!this.processLoginSession(response.getRawBody())) {
return StateCode.FAIL;
}
return StateCode.SUCCESS;
}
return matcher.group(1);
}
return StateCode.FAIL;
}
/**
* 处理登录session
*
* @param loginContent 登录text
* @return 返回是否处理成功
*/
private boolean processLoginSession(String loginContent) {
LoginSession loginSession = bot.session();
Matcher matcher = PROCESS_LOGIN_PATTERN.matcher(loginContent);
if (matcher.find()) {
loginSession.setUrl(matcher.group(1));
}
ApiResponse response = this.client.send(new StringRequest(loginSession.getUrl()).noRedirect());
loginSession.setUrl(loginSession.getUrl().substring(0, loginSession.getUrl().lastIndexOf("/")));
String body = response.getRawBody();
List fileUrl = new ArrayList<>();
List syncUrl = new ArrayList<>();
for (int i = 0; i < FILE_URL.size(); i++) {
fileUrl.add(String.format("https://%s/cgi-bin/mmwebwx-bin", FILE_URL.get(i)));
syncUrl.add(String.format("https://%s/cgi-bin/mmwebwx-bin", WEB_PUSH_URL.get(i)));
}
boolean flag = false;
for (int i = 0; i < FILE_URL.size(); i++) {
String indexUrl = INDEX_URL.get(i);
if (loginSession.getUrl().contains(indexUrl)) {
loginSession.setFileUrl(fileUrl.get(i));
loginSession.setSyncUrl(syncUrl.get(i));
flag = true;
break;
}
}
if (!flag) {
loginSession.setFileUrl(loginSession.getUrl());
loginSession.setSyncUrl(loginSession.getUrl());
}
loginSession.setDeviceId("e" + String.valueOf(System.currentTimeMillis()));
BaseRequest baseRequest = new BaseRequest();
loginSession.setBaseRequest(baseRequest);
loginSession.setSKey(WeChatUtils.match("(\\S+) ", body));
loginSession.setWxSid(WeChatUtils.match("(\\S+) ", body));
loginSession.setWxUin(WeChatUtils.match("(\\S+) ", body));
loginSession.setPassTicket(WeChatUtils.match("(\\S+) ", body));
baseRequest.setSkey(loginSession.getSKey());
baseRequest.setSid(loginSession.getWxSid());
baseRequest.setUin(loginSession.getWxUin());
baseRequest.setDeviceID(loginSession.getDeviceId());
return true;
}
/**
* 推送登录
*
* @return 返回uuid
*/
private String pushLogin() {
// this.autoLogin();
String uin = this.client.cookie("wxUin");
if (StringUtils.isEmpty(uin)) {
return null;
}
String url = String.format("https://%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s",
Constant.WEB_PUSH_URL.get(0), uin);
JsonResponse jsonResponse = this.client.send(new JsonRequest(url));
if (!jsonResponse.success()) {
return null;
}
return jsonResponse.getString("uuid");
}
/**
* 开启状态通知
*/
private void statusNotify() {
log.info("开启状态通知");
String url = String.format("%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s",
bot.session().getUrl(), bot.session().getPassTicket());
this.client.send(new JsonRequest(url).post().jsonBody()
.add("BaseRequest", bot.session().getBaseRequest())
.add("Code", 3)
.add("FromUserName", bot.session().getUserName())
.add("ToUserName", bot.session().getUserName())
.add("ClientMsgId", System.currentTimeMillis() / 1000));
}
/**
* web 初始化
*/
private void webInit() {
log.info("微信初始化...");
int r = (int) (-System.currentTimeMillis() / 1000) / 1579;
String url = String.format("%s/webwxinit?r=%d&pass_ticket=%s",
bot.session().getUrl(), r, bot.session().getPassTicket());
JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
.add("BaseRequest", bot.session().getBaseRequest()));
WebInitResponse webInitResponse = response.parse(WebInitResponse.class);
List contactList = webInitResponse.getContactList();
this.syncRecentContact(contactList);
Account account = webInitResponse.getAccount();
SyncKey syncKey = webInitResponse.getSyncKey();
bot.session().setInviteStartCount(webInitResponse.getInviteStartCount());
bot.session().setAccount(account);
bot.session().setUserName(account.getUserName());
bot.session().setNickName(account.getNickName());
bot.session().setSyncKey(syncKey);
}
/**
* 开启一个县城接收监听
*/
private void startRevive() {
bot.setRunning(true);
String threadName = "wechat-listener";
Set threadNames = Thread.getAllStackTraces().keySet().stream()
.map(Thread::getName).collect(Collectors.toSet());
if (!threadNames.contains(threadName)) {
Thread thread = new Thread(new ChatLoop(bot));
thread.setName("wechat-listener");
thread.setDaemon(true);
thread.start();
}
}
/**
* 心跳检查
*
* @return SyncCheckRet
*/
@Override
public SyncCheckRet syncCheck() {
String url = String.format("%s/synccheck", bot.session().getSyncOrUrl());
try {
ApiResponse response = this.client.send(new StringRequest(url)
.add("r", System.currentTimeMillis())
.add("skey", bot.session().getSKey())
.add("sid", bot.session().getWxSid())
.add("uin", bot.session().getWxUin())
.add("deviceid", bot.session().getDeviceId())
.add("synckey", bot.session().getSyncKeyStr())
.add("_", System.currentTimeMillis())
.timeout(30)
);
Matcher matcher = SYNC_CHECK_PATTERN.matcher(response.getRawBody());
if (matcher.find()) {
if (!"0".equals(matcher.group(1))) {
log.debug("Unexpected sync check result: {}", response.getRawBody());
return new SyncCheckRet(RetCode.parse(Integer.valueOf(matcher.group(1))), 0);
}
return new SyncCheckRet(RetCode.parse(Integer.valueOf(matcher.group(1))), Integer.valueOf(matcher.group(2)));
}
return new SyncCheckRet(RetCode.UNKNOWN, 0);
} catch (Exception e) {
if (e instanceof SocketTimeoutException) {
log.warn("心跳检查超时");
return syncCheck();
}
log.error("心跳检查出错", e);
return new SyncCheckRet(RetCode.UNKNOWN, 0);
}
}
/**
* 获取最新消息
*
* @return WebSyncResponse
*/
@Override
public WebSyncResponse webSync() {
String url = String.format("%s/webwxsync?sid=%s&sKey=%s&passTicket=%s",
bot.session().getUrl(), bot.session().getWxSid(),
bot.session().getSKey(), bot.session().getPassTicket());
JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
.add("BaseRequest", bot.session().getBaseRequest())
.add("SyncKey", bot.session().getSyncKey())
.add("rr", ~(System.currentTimeMillis() / 1000)));
WebSyncResponse webSyncResponse = response.parse(WebSyncResponse.class);
if (!webSyncResponse.success()) {
log.error("获取消息失败 {}", webSyncResponse.toString());
this.webInit();
throw new RuntimeException(String.format("获取消息(webSync)失败:%s", webSyncResponse.toString()));
}
bot.session().setSyncKey(webSyncResponse.getSyncKey());
return webSyncResponse;
}
/**
* 退出登录
*/
@Override
public void logout() {
if (bot.isRunning()) {
String url = String.format("%s/webwxlogout", bot.session().getUrl());
this.client.send(new StringRequest(url)
.add("redirect", 1)
.add("type", 1)
.add("sKey", bot.session().getSKey()));
bot.setRunning(false);
}
this.logging = false;
this.client.cookies().clear();
String file = bot.config().assetsDir() + "/login.json";
new File(file).delete();
}
/**
* 加载联系人信息
*
* @param seq 默认为0,当一次无法加载完全则大于0
*/
@Override
public void loadContact(int seq) {
log.info("开始获取联系人信息");
while (true) {
String url = String.format("%s/webwxgetcontact?r=%s&seq=%s&skey=%s",
bot.session().getUrl(), System.currentTimeMillis(),
seq, bot.session().getSKey());
JsonResponse response = this.client.send(new JsonRequest(url).jsonBody());
JsonObject jsonObject = response.toJsonObject();
seq = jsonObject.get("Seq").getAsInt();
this.memberCount += jsonObject.get("MemberCount").getAsInt();
List memberList = WeChatUtils.fromJson(WeChatUtils.toJson(jsonObject.getAsJsonArray("MemberList")), new TypeToken>() {
});
for (Account account : memberList) {
if (StringUtils.isNotEmpty(account.getUserName())) {
accountMap.put(account.getUserName(), account);
}
}
// 查看seq是否为0,0表示好友列表已全部获取完毕,若大于0,则表示好友列表未获取完毕,当前的字节数(断点续传)
if (seq == 0) {
break;
}
}
this.contactList = new ArrayList<>(this.getAccountByType(AccountType.TYPE_FRIEND));
this.publicUsersList = new ArrayList<>(this.getAccountByType(AccountType.TYPE_MP));
this.specialUsersList = new ArrayList<>(this.getAccountByType(AccountType.TYPE_SPECIAL));
Set groupAccounts = this.getAccountByType(AccountType.TYPE_GROUP);
for (Account groupAccount : groupAccounts) {
groupUserNames.add(groupAccount.getUserName());
}
}
/**
* 加载群信息
*/
public void loadGroupList() {
log.info("加载群聊信息");
// 群账号
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy