Merge pull request #228 from PathfinderAx/master

feat(0): [java]-[wcferry-mvn]-集成插件及模块规范化
This commit is contained in:
Changhua 2024-09-23 11:55:00 +08:00 committed by GitHub
commit f2e467663b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2853 additions and 2449 deletions

View File

@ -47,7 +47,7 @@ wcferry:
### 编译运行 ### 编译运行
找到 src/main/java/com/iamteer/Main.java 类 找到 src/main/java/com/iamteer/WcferryApplication.java 类
直接启动即可 直接启动即可
@ -55,8 +55,6 @@ wcferry:
启动后springboot自身的端口为 9201 socket的端口为 10086 启动后springboot自身的端口为 9201 socket的端口为 10086
## 参与开发 ## 参与开发
### 核心依赖 ### 核心依赖
@ -68,5 +66,74 @@ wcferry:
| jna | 5.6.0 | 态访问系统本地库 | | jna | 5.6.0 | 态访问系统本地库 |
| nng-java | 1.4.0 | 本地包 | | nng-java | 1.4.0 | 本地包 |
### 模块结构
```
wcferry-mvn
├─dll 核心dll
│ ├─sdk.dll sdk文件
│ └─readme.txt 本目录说明文件
├─src 源
│ ├─main 重启命令
│ │ ├─java(com.iamteer) java代码目录
│ │ │ ├─config 配置
│ │ │ ├─entity 实体
│ │ │ ├─runner 运行(程序启动中与启动后的自动化任务都放置于此)
│ │ │ ├─service 业务接口
│ │ │ │ └─impl 业务实现类
│ │ │ ├─Client.java socket客户端
│ │ │ └─WcferryApplication.java 启动类
│ │ │resources 数据库脚本
│ │ │ ├─libs 本程序内置依赖包
│ │ │ ├─proto proto文件
│ │ │ ├─win32-x86-64 依赖程序
│ │ │ ├─application.yml 本程序主配置文件
│ │ │ └─logback-spring.xml 日志配置文件
├─pom.xml POM文件
├─README.MD 说明文件
```
### 配置说明
本程序主配置文件为 application.yml
#### 配置参数
本程序内置参数统一前缀wcferry 所有自定义本服务的参数请都放置在此前缀下,如:
```ymal
wcferry:
# DLL文件位置
dll-path: /dll/sdk.dll
```
#### 生成proto文件
本程序已经集成了生成proto文件的maven插件直接install即可生成proto文件且会在打包程序中去除 src/main/resources/proto
下面的内容,只保留实体类中的文件
默认install会重新根据 生成实体,如果不想被替换,请删除 src/main/resources/proto 下对应的文件即可
### 提交规范
本模块希望大家使用统一提交格式,便于区分
格式:类型(任务号/缺陷号/没有使用0替代): [模块名称]-[子模块名称]-本次修改的说明
如:
```cmd
feat(0): [java]-[wcferry-mvn]-基础类目录划分迁移及代码格式
```
| 名称 | 版本 |
|------|--------------|
| feat | 新功能 |
| fix | 缺陷 |
| ... | 其他等git规范中的均可 |

3
clients/java/wcferry-mvn/dll/.gitkeep vendored Normal file
View File

@ -0,0 +1,3 @@
# Ignore everything in this directory
*
# Except this file !.gitkeep

View File

@ -0,0 +1,6 @@
说明:
由于项目规范限制本目录保留但最新的DLL还需要各位自行去下载解压
请把最新的 https://github.com/lich0821/WeChatFerry/releases/latest 下载的文件解压后放到该目录下
sdk.dll
spy.dll
spy_debug.dll

View File

@ -61,4 +61,54 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<!-- 同时添加maven插件用于编译protobuf生成java文件 -->
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<!-- 注意,需要与 com.google.protobuf:protobuf-java 一致 -->
<protocArtifact>
com.google.protobuf:protoc:3.22.2:exe:${os.detected.classifier}
</protocArtifact>
<!-- 默认值proto源文件路径 -->
<protoSourceRoot>${project.basedir}/src/main/resources/proto</protoSourceRoot>
<!-- 默认值proto目标java文件路径如果不指定则只在jar与编译中生成 -->
<outputDirectory>src/main/java</outputDirectory>
<pluginId>grpc-java</pluginId>
<!-- 设置是否在生成java文件之前清空outputDirectory的文件默认值为true设置为false时也会覆盖同名文件 -->
<clearOutputDirectory>false</clearOutputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<excludes>
<exclude>**/*.proto</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View File

@ -8,529 +8,528 @@ import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import com.sun.jna.Native; import com.iamteer.service.SDK;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.iamteer.Wcf.*; import com.iamteer.entity.Wcf;
import com.iamteer.entity.Wcf.DbQuery;
import com.iamteer.entity.Wcf.DbRow;
import com.iamteer.entity.Wcf.DbTable;
import com.iamteer.entity.Wcf.DecPath;
import com.iamteer.entity.Wcf.Functions;
import com.iamteer.entity.Wcf.MemberMgmt;
import com.iamteer.entity.Wcf.Request;
import com.iamteer.entity.Wcf.Response;
import com.iamteer.entity.Wcf.RpcContact;
import com.iamteer.entity.Wcf.UserInfo;
import com.iamteer.entity.Wcf.Verification;
import com.iamteer.entity.Wcf.WxMsg;
import com.sun.jna.Native;
import io.sisu.nng.Socket; import io.sisu.nng.Socket;
import io.sisu.nng.pair.Pair1Socket; import io.sisu.nng.pair.Pair1Socket;
public class Client { public class Client {
private static final Logger logger = LoggerFactory.getLogger(Client.class); private static final Logger logger = LoggerFactory.getLogger(Client.class);
private static final int BUFFER_SIZE = 16 * 1024 * 1024; // 16M private static final int BUFFER_SIZE = 16 * 1024 * 1024; // 16M
private Socket cmdSocket = null; private Socket cmdSocket = null;
private Socket msgSocket = null; private Socket msgSocket = null;
private static String DEFAULT_HOST = "127.0.0.1"; private static String DEFAULT_HOST = "127.0.0.1";
private static int PORT = 10086; private static int PORT = 10086;
private static String CMDURL = "tcp://%s:%s"; private static String CMDURL = "tcp://%s:%s";
private static String DEFAULT_DLL_PATH = System.getProperty("user.dir") + "\\dll\\sdk.dll"; private static String DEFAULT_DLL_PATH = System.getProperty("user.dir") + "\\dll\\sdk.dll";
private boolean isReceivingMsg = false; private boolean isReceivingMsg = false;
private boolean isLocalHostPort = false; private boolean isLocalHostPort = false;
private BlockingQueue<WxMsg> msgQ; private BlockingQueue<WxMsg> msgQ;
private String host; private String host;
private int port; private int port;
private String dllPath; private String dllPath;
public Client() { public Client() {
this(DEFAULT_HOST, PORT, false, DEFAULT_DLL_PATH); this(DEFAULT_HOST, PORT, false, DEFAULT_DLL_PATH);
}
public Client(int port, String dllPath) {
this(DEFAULT_HOST, port, false, dllPath);
}
public Client(String host, int port, boolean debug, String dllPath) {
this.host = host;
this.port = port;
this.dllPath = dllPath;
SDK INSTANCE = Native.load(dllPath, SDK.class);
int status = INSTANCE.WxInitSDK(debug, port);
if (status != 0) {
logger.error("启动 RPC 失败: {}", status);
System.exit(-1);
}
connectRPC(String.format(CMDURL, host, port), INSTANCE);
if (DEFAULT_HOST.equals(host) || "localhost".equalsIgnoreCase(host)) {
isLocalHostPort = true;
}
}
public void connectRPC(String url, SDK INSTANCE) {
try {
cmdSocket = new Pair1Socket();
cmdSocket.dial(url);
//logger.info("请点击登录微信");
while (!isLogin()) { // 直到登录成功
waitMs(1000);
}
} catch (Exception e) {
logger.error("连接 RPC 失败: ", e);
System.exit(-1);
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("关闭...");
diableRecvMsg();
if (isLocalHostPort) {
INSTANCE.WxDestroySDK();
}
}));
}
private Response sendCmd(Request req) {
try {
ByteBuffer bb = ByteBuffer.wrap(req.toByteArray());
cmdSocket.send(bb);
ByteBuffer ret = ByteBuffer.allocate(BUFFER_SIZE);
long size = cmdSocket.receive(ret, true);
return Response.parseFrom(Arrays.copyOfRange(ret.array(), 0, (int) size));
} catch (Exception e) {
logger.error("命令调用失败: ", e);
return null;
}
}
/**
* 当前微信客户端是否登录微信号
*
* @return
*/
public boolean isLogin() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_IS_LOGIN_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getStatus() == 1;
}
return false;
}
/**
* 获得微信客户端登录的微信ID
*
* @return
*/
public String getSelfWxid() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_SELF_WXID_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getStr();
} }
return ""; public Client(int port, String dllPath) {
} this(DEFAULT_HOST, port, false, dllPath);
/**
* 获取所有消息类型
*
* @return
*/
public Map<Integer, String> getMsgTypes() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_MSG_TYPES_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getTypes().getTypesMap();
} }
return Wcf.MsgTypes.newBuilder().build().getTypesMap(); public Client(String host, int port, boolean debug, String dllPath) {
} this.host = host;
this.port = port;
this.dllPath = dllPath;
/** SDK INSTANCE = Native.load(dllPath, SDK.class);
* 获取所有联系人 int status = INSTANCE.WxInitSDK(debug, port);
* "fmessage": "朋友推荐消息", if (status != 0) {
* "medianote": "语音记事本", logger.error("启动 RPC 失败: {}", status);
* "floatbottle": "漂流瓶", System.exit(-1);
* "filehelper": "文件传输助手", }
* "newsapp": "新闻", connectRPC(String.format(CMDURL, host, port), INSTANCE);
* if (DEFAULT_HOST.equals(host) || "localhost".equalsIgnoreCase(host)) {
* @return isLocalHostPort = true;
*/ }
public List<RpcContact> getContacts() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_CONTACTS_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getContacts().getContactsList();
} }
return Wcf.RpcContacts.newBuilder().build().getContactsList(); public void connectRPC(String url, SDK INSTANCE) {
} try {
cmdSocket = new Pair1Socket();
/** cmdSocket.dial(url);
* 获取sql执行结果 // logger.info("请点击登录微信");
* while (!isLogin()) { // 直到登录成功
* @param db 数据库名 waitMs(1000);
* @param sql 执行的sql语句 }
* @return } catch (Exception e) {
*/ logger.error("连接 RPC 失败: ", e);
public List<DbRow> querySql(String db, String sql) { System.exit(-1);
DbQuery dbQuery = DbQuery.newBuilder().setSql(sql).setDb(db).build(); }
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_EXEC_DB_QUERY_VALUE) Runtime.getRuntime().addShutdownHook(new Thread(() -> {
.setQuery(dbQuery).build(); logger.info("关闭...");
Response rsp = sendCmd(req); diableRecvMsg();
if (rsp != null) { if (isLocalHostPort) {
return rsp.getRows().getRowsList(); INSTANCE.WxDestroySDK();
} }
return null; }));
}
/**
* 获取所有数据库名
*
* @return
*/
public List<String> getDbNames() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_DB_NAMES_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getDbs().getNamesList();
} }
return Wcf.DbNames.newBuilder().build().getNamesList(); private Response sendCmd(Request req) {
} try {
ByteBuffer bb = ByteBuffer.wrap(req.toByteArray());
/** cmdSocket.send(bb);
* 获取指定数据库中的所有表 ByteBuffer ret = ByteBuffer.allocate(BUFFER_SIZE);
* long size = cmdSocket.receive(ret, true);
* @param db return Response.parseFrom(Arrays.copyOfRange(ret.array(), 0, (int)size));
* @return } catch (Exception e) {
*/ logger.error("命令调用失败: ", e);
public Map<String, String> getDbTables(String db) { return null;
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_DB_TABLES_VALUE).setStr(db) }
.build();
Response rsp = sendCmd(req);
Map<String, String> tables = new HashMap<>();
if (rsp != null) {
for (DbTable tbl : rsp.getTables().getTablesList()) {
tables.put(tbl.getName(), tbl.getSql());
}
} }
return tables; /**
} * 当前微信客户端是否登录微信号
*
/** * @return
* @param msg: 消息内容如果是 @ 消息则需要有跟 @ 的人数量相同的 @ */
* @param receiver: 消息接收人私聊为 wxidwxid_xxxxxxxxxxxxxx群聊为 public boolean isLogin() {
* roomidxxxxxxxxxx@chatroom Request req = Request.newBuilder().setFuncValue(Functions.FUNC_IS_LOGIN_VALUE).build();
* @param aters: 群聊时要 @ 的人私聊时为空字符串多个用逗号分隔@所有人 Response rsp = sendCmd(req);
* notify@all必须是群主或者管理员才有权限 if (rsp != null) {
* @return int return rsp.getStatus() == 1;
* @Description 发送文本消息 }
* @author Changhua return false;
* @example sendText(" Hello @ 某人1 @ 某人2 ", " xxxxxxxx @ chatroom ",
* "wxid_xxxxxxxxxxxxx1,wxid_xxxxxxxxxxxxx2");
**/
public int sendText(String msg, String receiver, String aters) {
Wcf.TextMsg textMsg = Wcf.TextMsg.newBuilder().setMsg(msg).setReceiver(receiver).setAters(aters)
.build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_TXT_VALUE).setTxt(textMsg)
.build();
logger.debug("sendText: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
} }
return ret; /**
} * 获得微信客户端登录的微信ID
*
* @return
*/
public String getSelfWxid() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_SELF_WXID_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getStr();
}
/** return "";
* 发送图片消息
*
* @param path 图片地址
* @param receiver 接收者微信id
* @return 发送结果状态码
*/
public int sendImage(String path, String receiver) {
Wcf.PathMsg pathMsg = Wcf.PathMsg.newBuilder().setPath(path).setReceiver(receiver).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_IMG_VALUE).setFile(pathMsg)
.build();
logger.debug("sendImage: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
} }
return ret; /**
} * 获取所有消息类型
*
* @return
*/
public Map<Integer, String> getMsgTypes() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_MSG_TYPES_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getTypes().getTypesMap();
}
/** return Wcf.MsgTypes.newBuilder().build().getTypesMap();
* 发送文件消息
*
* @param path 文件地址
* @param receiver 接收者微信id
* @return 发送结果状态码
*/
public int sendFile(String path, String receiver) {
Wcf.PathMsg pathMsg = Wcf.PathMsg.newBuilder().setPath(path).setReceiver(receiver).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_FILE_VALUE).setFile(pathMsg)
.build();
logger.debug("sendFile: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
} }
return ret; /**
} * 获取所有联系人
* "fmessage": "朋友推荐消息",
* "medianote": "语音记事本",
* "floatbottle": "漂流瓶",
* "filehelper": "文件传输助手",
* "newsapp": "新闻",
*
* @return
*/
public List<RpcContact> getContacts() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_CONTACTS_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getContacts().getContactsList();
}
/** return Wcf.RpcContacts.newBuilder().build().getContactsList();
* 发送Xml消息
*
* @param receiver 接收者微信id
* @param xml xml内容
* @param path
* @param type
* @return 发送结果状态码
*/
public int sendXml(String receiver, String xml, String path, int type) {
Wcf.XmlMsg xmlMsg = Wcf.XmlMsg.newBuilder().setContent(xml).setReceiver(receiver).setPath(path)
.setType(type).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_XML_VALUE).setXml(xmlMsg)
.build();
logger.debug("sendXml: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
} }
return ret; /**
} * 获取sql执行结果
*
/** * @param db 数据库名
* 发送表情消息 * @param sql 执行的sql语句
* * @return
* @param path 表情路径 */
* @param receiver 消息接收者 public List<DbRow> querySql(String db, String sql) {
* @return 发送结果状态码 DbQuery dbQuery = DbQuery.newBuilder().setSql(sql).setDb(db).build();
*/ Request req = Request.newBuilder().setFuncValue(Functions.FUNC_EXEC_DB_QUERY_VALUE).setQuery(dbQuery).build();
public int sendEmotion(String path, String receiver) { Response rsp = sendCmd(req);
Wcf.PathMsg pathMsg = Wcf.PathMsg.newBuilder().setPath(path).setReceiver(receiver).build(); if (rsp != null) {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_EMOTION_VALUE) return rsp.getRows().getRowsList();
.setFile(pathMsg).build(); }
logger.debug("sendEmotion: {}", bytesToHex(req.toByteArray())); return null;
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
} }
return ret; /**
} * 获取所有数据库名
*
* @return
*/
public List<String> getDbNames() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_DB_NAMES_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getDbs().getNamesList();
}
/** return Wcf.DbNames.newBuilder().build().getNamesList();
* 接收好友请求
*
* @param v3 xml.attrib["encryptusername"]
* @param v4 xml.attrib["ticket"]
* @return 结果状态码
*/
public int acceptNewFriend(String v3, String v4) {
int ret = -1;
Verification verification = Verification.newBuilder().setV3(v3).setV4(v4).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_ACCEPT_FRIEND_VALUE)
.setV(verification).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
}
return ret;
}
/**
* 添加群成员为微信好友
*
* @param roomID 群ID
* @param wxIds 要加群的人列表逗号分隔
* @return 1 为成功其他失败
*/
public int addChatroomMembers(String roomID, String wxIds) {
int ret = -1;
MemberMgmt memberMgmt = MemberMgmt.newBuilder().setRoomid(roomID).setWxids(wxIds).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_ADD_ROOM_MEMBERS_VALUE)
.setM(memberMgmt).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
}
return ret;
}
/**
* 解密图片
*
* @param srcPath 加密的图片路径
* @param dstPath 解密的图片路径
* @return 是否成功
*/
public boolean decryptImage(String srcPath, String dstPath) {
int ret = -1;
DecPath build = DecPath.newBuilder().setSrc(srcPath).setDst(dstPath).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_DECRYPT_IMAGE_VALUE)
.setDec(build).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
}
return ret == 1;
}
/**
* 获取个人信息
*
* @return 个人信息
*/
public UserInfo getUserInfo() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_USER_INFO_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getUi();
}
return null;
}
public boolean getIsReceivingMsg() {
return isReceivingMsg;
}
public WxMsg getMsg() {
try {
return msgQ.take();
} catch (Exception e) {
// TODO: handle exception
return null;
}
}
/**
* 判断是否是艾特自己的消息
*
* @param wxMsgXml
* @param wxMsgContent
* @return
*/
public boolean isAtMeMsg(String wxMsgXml, String wxMsgContent) {
String format = String.format("<atuserlist><![CDATA[%s]]></atuserlist>", getSelfWxid());
boolean isAtAll = wxMsgContent.startsWith("@所有人") || wxMsgContent.startsWith("@all");
if (wxMsgXml.contains(format) && !isAtAll) {
return true;
}
return false;
}
private void listenMsg(String url) {
try {
msgSocket = new Pair1Socket();
msgSocket.dial(url);
msgSocket.setReceiveTimeout(2000); // 2 秒超时
} catch (Exception e) {
logger.error("创建消息 RPC 失败", e);
return;
}
ByteBuffer bb = ByteBuffer.allocate(BUFFER_SIZE);
while (isReceivingMsg) {
try {
long size = msgSocket.receive(bb, true);
WxMsg wxMsg = Response.parseFrom(Arrays.copyOfRange(bb.array(), 0, (int) size)).getWxmsg();
msgQ.put(wxMsg);
} catch (Exception e) {
// 多半是超时忽略吧
}
}
try {
msgSocket.close();
} catch (Exception e) {
logger.error("关闭连接失败", e);
}
}
public void enableRecvMsg(int qSize) {
if (isReceivingMsg) {
return;
} }
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_ENABLE_RECV_TXT_VALUE).build(); /**
Response rsp = sendCmd(req); * 获取指定数据库中的所有表
if (rsp == null) { *
logger.error("启动消息接收失败"); * @param db
isReceivingMsg = false; * @return
return; */
public Map<String, String> getDbTables(String db) {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_DB_TABLES_VALUE).setStr(db).build();
Response rsp = sendCmd(req);
Map<String, String> tables = new HashMap<>();
if (rsp != null) {
for (DbTable tbl : rsp.getTables().getTablesList()) {
tables.put(tbl.getName(), tbl.getSql());
}
}
return tables;
} }
isReceivingMsg = true; /**
msgQ = new ArrayBlockingQueue<>(qSize); * @param msg: 消息内容如果是 @ 消息则需要有跟 @ 的人数量相同的 @
String msgUrl = "tcp://" + this.host + ":" + (this.port + 1); * @param receiver: 消息接收人私聊为 wxidwxid_xxxxxxxxxxxxxx群聊为
Thread thread = new Thread(() -> listenMsg(msgUrl)); * roomidxxxxxxxxxx@chatroom
thread.start(); * @param aters: 群聊时要 @ 的人私聊时为空字符串多个用逗号分隔@所有人
} * notify@all必须是群主或者管理员才有权限
* @return int
* @Description 发送文本消息
* @author Changhua
* @example sendText(" Hello @ 某人1 @ 某人2 ", " xxxxxxxx @ chatroom ",
* "wxid_xxxxxxxxxxxxx1,wxid_xxxxxxxxxxxxx2");
**/
public int sendText(String msg, String receiver, String aters) {
Wcf.TextMsg textMsg = Wcf.TextMsg.newBuilder().setMsg(msg).setReceiver(receiver).setAters(aters).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_TXT_VALUE).setTxt(textMsg).build();
logger.debug("sendText: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
}
public int diableRecvMsg() { return ret;
if (!isReceivingMsg) {
return 1;
} }
int ret = -1;
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_DISABLE_RECV_TXT_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
if (ret == 0) {
isReceivingMsg = false;
}
/**
* 发送图片消息
*
* @param path 图片地址
* @param receiver 接收者微信id
* @return 发送结果状态码
*/
public int sendImage(String path, String receiver) {
Wcf.PathMsg pathMsg = Wcf.PathMsg.newBuilder().setPath(path).setReceiver(receiver).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_IMG_VALUE).setFile(pathMsg).build();
logger.debug("sendImage: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
}
return ret;
} }
return ret;
}
public void waitMs(int ms) { /**
try { * 发送文件消息
Thread.sleep(ms); *
} catch (InterruptedException ex) { * @param path 文件地址
Thread.currentThread().interrupt(); * @param receiver 接收者微信id
* @return 发送结果状态码
*/
public int sendFile(String path, String receiver) {
Wcf.PathMsg pathMsg = Wcf.PathMsg.newBuilder().setPath(path).setReceiver(receiver).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_FILE_VALUE).setFile(pathMsg).build();
logger.debug("sendFile: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
}
return ret;
} }
}
public void printContacts(List<RpcContact> contacts) { /**
for (RpcContact c : contacts) { * 发送Xml消息
int value = c.getGender(); *
String gender; * @param receiver 接收者微信id
if (value == 1) { * @param xml xml内容
gender = ""; * @param path
} else if (value == 2) { * @param type
gender = ""; * @return 发送结果状态码
} else { */
gender = "未知"; public int sendXml(String receiver, String xml, String path, int type) {
} Wcf.XmlMsg xmlMsg = Wcf.XmlMsg.newBuilder().setContent(xml).setReceiver(receiver).setPath(path).setType(type).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_XML_VALUE).setXml(xmlMsg).build();
logger.debug("sendXml: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
}
logger.info("{}, {}, {}, {}, {}, {}, {}", c.getWxid(), c.getName(), c.getCode(), return ret;
c.getCountry(), c.getProvince(), c.getCity(), gender);
} }
}
public void printWxMsg(WxMsg msg) { /**
logger.info("{}[{}]:{}:{}:{}\n{}", msg.getSender(), msg.getRoomid(), msg.getId(), msg.getType(), * 发送表情消息
msg.getXml().replace("\n", "").replace("\t", ""), msg.getContent()); *
} * @param path 表情路径
* @param receiver 消息接收者
* @return 发送结果状态码
*/
public int sendEmotion(String path, String receiver) {
Wcf.PathMsg pathMsg = Wcf.PathMsg.newBuilder().setPath(path).setReceiver(receiver).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_SEND_EMOTION_VALUE).setFile(pathMsg).build();
logger.debug("sendEmotion: {}", bytesToHex(req.toByteArray()));
Response rsp = sendCmd(req);
int ret = -1;
if (rsp != null) {
ret = rsp.getStatus();
}
private String bytesToHex(byte[] bytes) { return ret;
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
} }
return sb.toString();
}
public void keepRunning() { /**
while (true) { * 接收好友请求
waitMs(1000); *
* @param v3 xml.attrib["encryptusername"]
* @param v4 xml.attrib["ticket"]
* @return 结果状态码
*/
public int acceptNewFriend(String v3, String v4) {
int ret = -1;
Verification verification = Verification.newBuilder().setV3(v3).setV4(v4).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_ACCEPT_FRIEND_VALUE).setV(verification).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
}
return ret;
} }
}
/**
* 添加群成员为微信好友
*
* @param roomID 群ID
* @param wxIds 要加群的人列表逗号分隔
* @return 1 为成功其他失败
*/
public int addChatroomMembers(String roomID, String wxIds) {
int ret = -1;
MemberMgmt memberMgmt = MemberMgmt.newBuilder().setRoomid(roomID).setWxids(wxIds).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_ADD_ROOM_MEMBERS_VALUE).setM(memberMgmt).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
}
return ret;
}
/**
* 解密图片
*
* @param srcPath 加密的图片路径
* @param dstPath 解密的图片路径
* @return 是否成功
*/
public boolean decryptImage(String srcPath, String dstPath) {
int ret = -1;
DecPath build = DecPath.newBuilder().setSrc(srcPath).setDst(dstPath).build();
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_DECRYPT_IMAGE_VALUE).setDec(build).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
}
return ret == 1;
}
/**
* 获取个人信息
*
* @return 个人信息
*/
public UserInfo getUserInfo() {
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_GET_USER_INFO_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
return rsp.getUi();
}
return null;
}
public boolean getIsReceivingMsg() {
return isReceivingMsg;
}
public WxMsg getMsg() {
try {
return msgQ.take();
} catch (Exception e) {
// TODO: handle exception
return null;
}
}
/**
* 判断是否是艾特自己的消息
*
* @param wxMsgXml
* @param wxMsgContent
* @return
*/
public boolean isAtMeMsg(String wxMsgXml, String wxMsgContent) {
String format = String.format("<atuserlist><![CDATA[%s]]></atuserlist>", getSelfWxid());
boolean isAtAll = wxMsgContent.startsWith("@所有人") || wxMsgContent.startsWith("@all");
if (wxMsgXml.contains(format) && !isAtAll) {
return true;
}
return false;
}
private void listenMsg(String url) {
try {
msgSocket = new Pair1Socket();
msgSocket.dial(url);
msgSocket.setReceiveTimeout(2000); // 2 秒超时
} catch (Exception e) {
logger.error("创建消息 RPC 失败", e);
return;
}
ByteBuffer bb = ByteBuffer.allocate(BUFFER_SIZE);
while (isReceivingMsg) {
try {
long size = msgSocket.receive(bb, true);
WxMsg wxMsg = Response.parseFrom(Arrays.copyOfRange(bb.array(), 0, (int)size)).getWxmsg();
msgQ.put(wxMsg);
} catch (Exception e) {
// 多半是超时忽略吧
}
}
try {
msgSocket.close();
} catch (Exception e) {
logger.error("关闭连接失败", e);
}
}
public void enableRecvMsg(int qSize) {
if (isReceivingMsg) {
return;
}
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_ENABLE_RECV_TXT_VALUE).build();
Response rsp = sendCmd(req);
if (rsp == null) {
logger.error("启动消息接收失败");
isReceivingMsg = false;
return;
}
isReceivingMsg = true;
msgQ = new ArrayBlockingQueue<>(qSize);
String msgUrl = "tcp://" + this.host + ":" + (this.port + 1);
Thread thread = new Thread(() -> listenMsg(msgUrl));
thread.start();
}
public int diableRecvMsg() {
if (!isReceivingMsg) {
return 1;
}
int ret = -1;
Request req = Request.newBuilder().setFuncValue(Functions.FUNC_DISABLE_RECV_TXT_VALUE).build();
Response rsp = sendCmd(req);
if (rsp != null) {
ret = rsp.getStatus();
if (ret == 0) {
isReceivingMsg = false;
}
}
return ret;
}
public void waitMs(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
public void printContacts(List<RpcContact> contacts) {
for (RpcContact c : contacts) {
int value = c.getGender();
String gender;
if (value == 1) {
gender = "";
} else if (value == 2) {
gender = "";
} else {
gender = "未知";
}
logger.info("{}, {}, {}, {}, {}, {}, {}", c.getWxid(), c.getName(), c.getCode(), c.getCountry(), c.getProvince(), c.getCity(), gender);
}
}
public void printWxMsg(WxMsg msg) {
logger.info("{}[{}]:{}:{}:{}\n{}", msg.getSender(), msg.getRoomid(), msg.getId(), msg.getType(),
msg.getXml().replace("\n", "").replace("\t", ""), msg.getContent());
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public void keepRunning() {
while (true) {
waitMs(1000);
}
}
} }

View File

@ -0,0 +1,35 @@
package com.iamteer.config;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
* 配置类-protobuf
*
* @author chandler
* @date 2024-09-26 21:35
*/
@Configuration
public class ProtobufConfig {
/**
* protobuf 序列化
*/
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
/**
* protobuf 反序列化
*/
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter protobufHttpMessageConverter) {
return new RestTemplate(Collections.singletonList(protobufHttpMessageConverter));
}
}

View File

@ -6,10 +6,10 @@ import org.springframework.stereotype.Component;
import lombok.Data; import lombok.Data;
/** /**
* 配置文件-UAM模块的外部接口 * 配置文件-wcferry的配置文件
* *
* @author chandler * @author chandler
* @date 2024-04-26 21:35 * @date 2024-09-21 21:35
*/ */
@Data @Data
@Component @Component
@ -22,7 +22,7 @@ public class WcferryProperties {
private String dllPath; private String dllPath;
/** /**
* 端口 * socket端口
*/ */
private Integer socketPort; private Integer socketPort;

View File

@ -2,7 +2,6 @@ package com.iamteer.runner;
import javax.annotation.Resource; import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@ -1,4 +1,4 @@
package com.iamteer; package com.iamteer.service;
import com.sun.jna.Library; import com.sun.jna.Library;

View File

@ -0,0 +1,3 @@
# Ignore everything in this directory
*
# Except this file !.gitkeep

View File

@ -0,0 +1,3 @@
# Ignore everything in this directory
*
# Except this file !.gitkeep

View File

@ -0,0 +1,236 @@
syntax = "proto3";
package wcf;
option java_package = "com.iamteer.entity";
enum Functions {
FUNC_RESERVED = 0x00;
FUNC_IS_LOGIN = 0x01;
FUNC_GET_SELF_WXID = 0x10;
FUNC_GET_MSG_TYPES = 0x11;
FUNC_GET_CONTACTS = 0x12;
FUNC_GET_DB_NAMES = 0x13;
FUNC_GET_DB_TABLES = 0x14;
FUNC_GET_USER_INFO = 0x15;
FUNC_GET_AUDIO_MSG = 0x16;
FUNC_SEND_TXT = 0x20;
FUNC_SEND_IMG = 0x21;
FUNC_SEND_FILE = 0x22;
FUNC_SEND_XML = 0x23;
FUNC_SEND_EMOTION = 0x24;
FUNC_SEND_RICH_TXT = 0x25;
FUNC_SEND_PAT_MSG = 0x26;
FUNC_FORWARD_MSG = 0x27;
FUNC_ENABLE_RECV_TXT = 0x30;
FUNC_DISABLE_RECV_TXT = 0x40;
FUNC_EXEC_DB_QUERY = 0x50;
FUNC_ACCEPT_FRIEND = 0x51;
FUNC_RECV_TRANSFER = 0x52;
FUNC_REFRESH_PYQ = 0x53;
FUNC_DOWNLOAD_ATTACH = 0x54;
FUNC_GET_CONTACT_INFO = 0x55;
FUNC_REVOKE_MSG = 0x56;
FUNC_REFRESH_QRCODE = 0x57;
FUNC_DECRYPT_IMAGE = 0x60;
FUNC_EXEC_OCR = 0x61;
FUNC_ADD_ROOM_MEMBERS = 0x70;
FUNC_DEL_ROOM_MEMBERS = 0x71;
FUNC_INV_ROOM_MEMBERS = 0x72;
}
message Request
{
Functions func = 1;
oneof msg
{
Empty empty = 2; //
string str = 3; //
TextMsg txt = 4; //
PathMsg file = 5; //
DbQuery query = 6; //
Verification v = 7; //
MemberMgmt m = 8; //
XmlMsg xml = 9; // XML参数结构
DecPath dec = 10; //
Transfer tf = 11; //
uint64 ui64 = 12 [ jstype = JS_STRING ]; // 64
bool flag = 13; //
AttachMsg att = 14; //
AudioMsg am = 15; //
RichText rt = 16; //
PatMsg pm = 17; //
ForwardMsg fm = 18; //
}
}
message Response
{
Functions func = 1;
oneof msg
{
int32 status = 2; // Int
string str = 3; //
WxMsg wxmsg = 4; //
MsgTypes types = 5; //
RpcContacts contacts = 6; //
DbNames dbs = 7; //
DbTables tables = 8; //
DbRows rows = 9; //
UserInfo ui = 10; //
OcrMsg ocr = 11; // OCR
};
}
message Empty { }
message WxMsg
{
bool is_self = 1; //
bool is_group = 2; //
uint64 id = 3 [ jstype = JS_STRING ]; // id
uint32 type = 4; //
uint32 ts = 5; //
string roomid = 6; // id
string content = 7; //
string sender = 8; //
string sign = 9; // Sign
string thumb = 10; //
string extra = 11; //
string xml = 12; // xml
}
message TextMsg
{
string msg = 1; //
string receiver = 2; // @
string aters = 3; // @
}
message PathMsg
{
string path = 1; //
string receiver = 2; //
}
message XmlMsg
{
string receiver = 1; //
string content = 2; // xml
string path = 3; //
int32 type = 4; //
}
message MsgTypes { map<int32, string> types = 1; }
message RpcContact
{
string wxid = 1; // id
string code = 2; //
string remark = 3; //
string name = 4; //
string country = 5; //
string province = 6; // /
string city = 7; //
int32 gender = 8; //
}
message RpcContacts { repeated RpcContact contacts = 1; }
message DbNames { repeated string names = 1; }
message DbTable
{
string name = 1; //
string sql = 2; // SQL
}
message DbTables { repeated DbTable tables = 1; }
message DbQuery
{
string db = 1; //
string sql = 2; // SQL
}
message DbField
{
int32 type = 1; //
string column = 2; //
bytes content = 3; //
}
message DbRow { repeated DbField fields = 1; }
message DbRows { repeated DbRow rows = 1; }
message Verification
{
string v3 = 1; //
string v4 = 2; // Ticket
int32 scene = 3; // 17 30
}
message MemberMgmt
{
string roomid = 1; // ID
string wxids = 2; //
}
message UserInfo
{
string wxid = 1; // ID
string name = 2; //
string mobile = 3; //
string home = 4; // /
}
message DecPath
{
string src = 1; //
string dst = 2; //
}
message Transfer
{
string wxid = 1; //
string tfid = 2; // id transferid
string taid = 3; // Transaction id
}
message AttachMsg
{
uint64 id = 1 [ jstype = JS_STRING ]; // id
string thumb = 2; // thumb
string extra = 3; // extra
}
message AudioMsg
{
uint64 id = 1 [ jstype = JS_STRING ]; // id
string dir = 2; //
}
message RichText
{
string name = 1; //
string account = 2; // id
string title = 3; //
string digest = 4; //
string url = 5; //
string thumburl = 6; //
string receiver = 7; //
}
message PatMsg
{
string roomid = 1; // id
string wxid = 2; // wxid
}
message OcrMsg
{
int32 status = 1; //
string result = 2; //
}
message ForwardMsg
{
uint64 id = 1 [ jstype = JS_STRING ]; // ID
string receiver = 2; // roomId wxid
}

View File

@ -0,0 +1,3 @@
# Ignore everything in this directory
*
# Except this file !.gitkeep

Binary file not shown.