├── .github
├── dependabot.yml
└── workflows
│ └── publish_jar.yml
├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── encodings.xml
├── jarRepositories.xml
├── misc.xml
├── uiDesigner.xml
└── vcs.xml
├── LICENSE
├── README.md
├── dependency-reduced-pom.xml
├── pom.xml
├── release_info.info
└── src
└── main
├── java
└── com
│ └── meteor
│ └── wechatbc
│ ├── Main.java
│ ├── WeChatCore.java
│ ├── command
│ ├── CommandExecutor.java
│ ├── WeChatCommand.java
│ └── sender
│ │ ├── CommandSender.java
│ │ ├── ConsoleSender.java
│ │ └── ContactSender.java
│ ├── config
│ ├── BaseConfigurationSection.java
│ ├── ConfigurationSection.java
│ ├── FileConfiguration.java
│ ├── SimpleConfigurationSection.java
│ └── YamlConfiguration.java
│ ├── entitiy
│ ├── SendMessage.java
│ ├── contact
│ │ ├── Contact.java
│ │ ├── ContactType.java
│ │ └── GetBatchContact.java
│ ├── message
│ │ ├── AppInfo.java
│ │ ├── Message.java
│ │ ├── RecommendInfo.java
│ │ └── SentMessage.java
│ ├── session
│ │ ├── BaseRequest.java
│ │ ├── Skey.java
│ │ └── SyncKey.java
│ └── synccheck
│ │ ├── SyncCheckResponse.java
│ │ ├── SyncCheckRetcode.java
│ │ └── SyncCheckSelector.java
│ ├── event
│ ├── Event.java
│ ├── EventBus.java
│ └── EventHandler.java
│ ├── impl
│ ├── DefaultPlugin.java
│ ├── HttpAPI.java
│ ├── HttpAPIImpl.java
│ ├── WeChatClient.java
│ ├── WeChatCoreImpl.java
│ ├── command
│ │ └── CommandManager.java
│ ├── console
│ │ └── Console.java
│ ├── contact
│ │ ├── ContactManager.java
│ │ ├── NickNameStrategy.java
│ │ ├── RemarkStrategy.java
│ │ ├── RetrievalStrategy.java
│ │ ├── RetrievalType.java
│ │ └── UserNameStrategy.java
│ ├── cookie
│ │ ├── CookiePack.java
│ │ └── WeChatCookie.java
│ ├── event
│ │ ├── EventManager.java
│ │ ├── Listener.java
│ │ ├── listener
│ │ │ └── ContactCommandListener.java
│ │ └── sub
│ │ │ ├── ClientDeathEvent.java
│ │ │ ├── GroupMessageEvent.java
│ │ │ ├── MessageEvent.java
│ │ │ ├── OwnerMessageEvent.java
│ │ │ └── ReceiveMessageEvent.java
│ ├── fileupload
│ │ ├── FileChunkUploader.java
│ │ └── model
│ │ │ ├── BaseResponse.java
│ │ │ ├── UploadMediaRequest.java
│ │ │ └── UploadResponse.java
│ ├── interceptor
│ │ └── WeChatInterceptor.java
│ ├── model
│ │ ├── MsgType.java
│ │ ├── Session.java
│ │ ├── WxInitInfo.java
│ │ └── message
│ │ │ ├── ImageEmoteMessage.java
│ │ │ ├── ImageMessage.java
│ │ │ ├── PayMessage.java
│ │ │ ├── RevokeMessage.java
│ │ │ ├── TextMessage.java
│ │ │ ├── VideoMessage.java
│ │ │ └── VoiceMessage.java
│ ├── plugin
│ │ ├── BasePlugin.java
│ │ └── PluginManager.java
│ ├── scheduler
│ │ └── SchedulerImpl.java
│ └── synccheck
│ │ ├── SyncCheckRunnable.java
│ │ └── message
│ │ ├── ConcreteMessageFactory.java
│ │ ├── MessageFactory.java
│ │ └── MessageProcessor.java
│ ├── launch
│ └── login
│ │ ├── DefaultPrintQRCodeCallBack.java
│ │ ├── PrintQRCodeCallBack.java
│ │ ├── WeChatLogin.java
│ │ ├── cokkie
│ │ └── WeChatCookieJar.java
│ │ └── model
│ │ ├── LoginMode.java
│ │ └── QRCodeResponse.java
│ ├── plugin
│ ├── Plugin.java
│ ├── PluginClassLoader.java
│ ├── PluginDescription.java
│ └── PluginLoader.java
│ ├── scheduler
│ ├── Scheduler.java
│ ├── Task.java
│ └── WeChatRunnable.java
│ └── util
│ ├── BaseConfig.java
│ ├── HttpUrlHelper.java
│ ├── RandomString.java
│ ├── URL.java
│ ├── VersionCheck.java
│ └── mode
│ └── GitHubRelease.java
└── resources
└── log4j2.xml
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "maven" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | target-branch: "master"
11 | schedule:
12 | interval: "daily"
13 |
--------------------------------------------------------------------------------
/.github/workflows/publish_jar.yml:
--------------------------------------------------------------------------------
1 | name: Release on Tag
2 |
3 | permissions:
4 | contents: write
5 | packages: write
6 |
7 | on:
8 | push:
9 | tags:
10 | - 'release-*'
11 | workflow_dispatch:
12 |
13 | jobs:
14 | build-and-release:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Set up JDK 8
20 | uses: actions/setup-java@v2
21 | with:
22 | java-version: '8'
23 | distribution: 'adopt'
24 |
25 | - name: Build with Maven
26 | run: mvn clean package
27 |
28 | - name: Extract Release Info
29 | id: release_info
30 | run: |
31 | echo "Reading release_info.info..."
32 | VERSION=$(awk -F= '/^VERSION=/ {print $2}' release_info.info)
33 | DESCRIPTION=$(awk '/^DESCRIPTION=/,EOF' release_info.info | sed '1 s/DESCRIPTION=//' | sed ':a;N;$!ba;s/\n/%0A/g')
34 | echo "::set-output name=version::${VERSION}"
35 | echo "::set-output name=description::${DESCRIPTION}"
36 |
37 | - name: Create GitHub Release
38 | uses: softprops/action-gh-release@v1
39 | with:
40 | tag_name: ${{ steps.release_info.outputs.version }}
41 | name: Release ${{ steps.release_info.outputs.version }}
42 | body: ${{ steps.release_info.outputs.description }}
43 | files: target/*.jar
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | /target/
3 | logs/
4 | plugins/
5 | voice/
6 | img/
7 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | logs/
10 | plugins/
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 zsh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | > 我是一名即将毕业的应届生,对计算机和Java后端开发充满热情。
3 | > 现在正在寻找实习工作,如果有符合的岗位招聘,希望能考虑一下我! 微信 zhengsenhe0758
4 |
5 | # WeChatBc
6 |
7 | > 类似开发公众号一样,开发个人微信号
8 |
9 | 采用了类似Bukkit的插件化框架设计,基于wechatbc独立且呼吸顺畅的开发扩展模块 ⭐
10 |
11 | 
12 |
13 | 
14 |
15 | (图片效果需安装 [WeChatSetu插件](https://github.com/meteorOSS/WeChatSetu) )
16 |
17 | 
18 |
19 | ## 支持功能
20 |
21 | * 消息回复(好友,群组),文本,视频,文件,语音发送
22 | * 获取用户信息,设置备注,添加好友....
23 | * API简单易用。使用java编写扩展插件,打包以jar载入运行 详见 [编写wechatbc插件](https://github.com/meteorOSS/WeChatBc/wiki/%E7%BC%96%E5%86%99WeChatBc%E6%8F%92%E4%BB%B6)
24 | * 文档 [WeChatBc-WIKI](https://github.com/meteorOSS/WeChatBc/wiki)
25 |
26 | ## 插件资源
27 |
28 | [📌 WeChatSetu: 让机器人随机发送"小姐姐跳舞视频";爬取pixiv图片](https://github.com/meteorOSS/WeChatSetu)
29 |
30 | [📌 chatgpt: 接入chatgpt](https://github.com/meteorOSS/wechat-gpt)
31 |
32 | [📌 revoke-listener: 防消息撤回](https://github.com/meteorOSS/revoke-listener)
33 |
34 | [📌 wechat-pay: 收款码收款回调](https://github.com/meteorOSS/wechat-pay)
35 |
36 | 你可以在 https://github.com/meteorOSS/wechat-bc/issues/28 分享你编写的插件,我会把它们更新到这里
37 |
38 | ## 安装
39 |
40 | 详见 [启动WeChatBc](https://github.com/meteorOSS/WeChatBc/wiki/%E5%90%AF%E5%8A%A8WeChatBc)
41 |
42 | ## Thanks
43 |
44 | > **[Bukkit框架](https://github.com/Bukkit/Bukkit)** 事件分发,插件管理等模式深受该项目影响
45 |
46 | > **[Jkook](https://github.com/SNWCreations/JKook)** 最初是因为该项目萌生了用java写一个微信客户端实现的想法
47 |
48 | > **[itchat](https://github.com/littlecodersh/itchat)** 调试微信接口时参考的项目,感谢前辈的付出
49 |
50 |
51 | 如今微信在当代社交生活中已然成为不可或缺的一环。本项目最初诞生于一个简单的愿景:
52 |
53 | 通过个人微信号,将用户与智能家居设备、快递查询服务以及日常事务通知等功能紧密连接。
54 |
55 | 目标不仅仅是简化操作流程,而是在于打造一个更加高效、便捷的生活方式
56 |
57 | 如果这一切能够为你的生活带来哪怕是微小的便捷与改变,对我而言,便是莫大的荣誉和满足
58 |
59 | ## 贡献
60 |
61 | 欢迎fork后提交pr,冲突解决需要花费很多时间,请在同步源仓库最新代码后进行提交
62 |
63 |
64 | ## ⭐使用中的任何问题欢迎提交issue或加入Q群653440235反馈
65 |
66 | 本项目仅供学习使用,一切使用本项目造成的后果,开发者概不负责
67 |
68 | [](https://star-history.com/#meteorOSS/wechat-bc&Date)
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/dependency-reduced-pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | com.meteor
5 | wechat-bc
6 | 1.1.8-SNAPSHOT
7 |
8 |
9 |
10 | maven-shade-plugin
11 | 3.2.4
12 |
13 |
14 | package
15 |
16 | shade
17 |
18 |
19 |
20 |
21 | *:*
22 |
23 |
24 |
25 |
26 |
27 | com.meteor.wechatbc.Main
28 |
29 |
30 | META-INF/spring.handlers
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | io.github.edwgiz
39 | log4j-maven-shade-plugin-extensions
40 | 2.17.1
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | jitpack.io
49 | https://jitpack.io
50 |
51 |
52 | fabricmc
53 | https://maven.fabricmc.net/
54 |
55 |
56 | architectury
57 | https://maven.architectury.dev/
58 |
59 |
60 |
61 |
62 | org.projectlombok
63 | lombok
64 | 1.18.30
65 | provided
66 |
67 |
68 | junit
69 | junit
70 | 4.13.1
71 | test
72 |
73 |
74 | hamcrest-core
75 | org.hamcrest
76 |
77 |
78 |
79 |
80 |
81 |
82 | github
83 | GitHub Apache Maven Packages
84 | https://maven.pkg.github.com/meteoross/wechat-bc
85 |
86 |
87 |
88 | github
89 | UTF-8
90 | 8
91 | 8
92 |
93 |
94 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.meteor
8 | wechat-bc
9 | 1.2-SNAPSHOT
10 |
11 |
12 |
13 | github
14 | GitHub Apache Maven Packages
15 | https://maven.pkg.github.com/meteoross/wechat-bc
16 |
17 |
18 |
19 |
20 |
21 | github
22 | 8
23 | 8
24 | UTF-8
25 |
26 |
27 |
28 |
29 | jitpack.io
30 | https://jitpack.io
31 |
32 |
33 | fabricmc
34 | https://maven.fabricmc.net/
35 |
36 |
37 | architectury
38 | https://maven.architectury.dev/
39 |
40 |
41 |
42 |
43 |
44 |
45 | org.apache.maven.plugins
46 | maven-shade-plugin
47 | 3.2.4
48 |
49 |
50 | package
51 |
52 | shade
53 |
54 |
55 |
56 |
57 | *:*
58 |
59 |
60 |
61 |
62 |
63 |
65 |
66 |
67 | com.meteor.wechatbc.Main
68 |
69 |
70 | META-INF/spring.handlers
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | io.github.edwgiz
79 | log4j-maven-shade-plugin-extensions
80 | 2.17.1
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | org.mybatis
93 | mybatis
94 | 3.4.5
95 |
96 |
97 |
98 | mysql
99 | mysql-connector-java
100 | 5.1.6
101 |
102 |
103 |
104 | com.github.ben-manes.caffeine
105 | caffeine
106 | 2.9.3
107 |
108 |
109 |
110 |
111 | org.projectlombok
112 | lombok
113 | 1.18.30
114 | provided
115 |
116 |
117 |
118 | org.yaml
119 | snakeyaml
120 | 2.2
121 |
122 |
123 |
124 | com.github.SNWCreations
125 | TerminalConsoleAppender
126 | 1.3.5
127 |
128 |
129 |
130 | org.apache.logging.log4j
131 | log4j-core
132 | 2.17.1
133 |
134 |
135 |
136 | com.alibaba
137 | fastjson
138 | 2.0.46
139 |
140 |
141 |
142 | junit
143 | junit
144 | 4.13.1
145 | test
146 |
147 |
148 |
149 | com.squareup.okhttp3
150 | okhttp
151 | 3.14.9
152 |
153 |
154 |
155 | org.jetbrains
156 | annotations
157 | 23.1.0
158 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/release_info.info:
--------------------------------------------------------------------------------
1 | VERSION=v1.2
2 | DESCRIPTION=fix: 获取语音消息时的类型转换错误
3 |
4 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/Main.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc;
2 |
3 | import com.meteor.wechatbc.impl.WeChatClient;
4 | import com.meteor.wechatbc.launch.login.DefaultPrintQRCodeCallBack;
5 | import com.meteor.wechatbc.plugin.PluginClassLoader;
6 | import com.meteor.wechatbc.util.VersionCheck;
7 | import org.apache.logging.log4j.LogManager;
8 | import org.apache.logging.log4j.Logger;
9 |
10 | import java.io.File;
11 | import java.net.URL;
12 |
13 |
14 | public class Main{
15 |
16 | private static final String MAIN_THREAD_NAME = "WECHATBC_MAIN";
17 |
18 | private Logger logger = LogManager.getLogger(MAIN_THREAD_NAME);
19 |
20 | public static void main(String[] args) {
21 | Thread.currentThread().setContextClassLoader(new PluginClassLoader(new URL[0], Main.class.getClassLoader()));
22 | System.exit(main0());
23 | }
24 |
25 | public static int main0() {
26 | Runtime.getRuntime().addShutdownHook(new Thread(() -> {
27 | if(weChatClient!=null){
28 | weChatClient.stop();
29 | }
30 | }));
31 | return new Main().start();
32 | }
33 |
34 | public static WeChatClient weChatClient = null;
35 |
36 |
37 | private void infoLogo(){
38 | System.out.println("\n" +
39 | " __ __ _____ _ _ ____ _____ \n" +
40 | " \\ \\ / / / ____| | | | | _ \\ / ____|\n" +
41 | " \\ \\ /\\ / /__| | | |__ __ _| |_ | |_) | | \n" +
42 | " \\ \\/ \\/ / _ \\ | | '_ \\ / _` | __| | _ <| | \n" +
43 | " \\ /\\ / __/ |____| | | | (_| | |_ | |_) | |____ \n" +
44 | " \\/ \\/ \\___|\\_____|_| |_|\\__,_|\\__| |____/ \\_____|\n" +
45 | " \n" +
46 | " \n");
47 | System.out.println("开源仓库: https://github.com/meteorOSS/WeChatBc");
48 | System.out.println("如果对你有帮助的话,请帮忙点个Star哦");
49 | VersionCheck.check("meteorOSS","wechat-bc");
50 | System.out.println("wechatbc 交流群: 653440235");
51 | }
52 |
53 | public int start(){
54 | this.infoLogo();
55 | Thread.currentThread().setName(MAIN_THREAD_NAME);
56 | weChatClient = new WeChatClient();
57 |
58 | // 登录
59 | weChatClient.login(new DefaultPrintQRCodeCallBack());
60 |
61 | try {
62 | weChatClient.getWeChatCore().getSession().saveHotLoginData(new File("hotLogin.dat"));
63 | weChatClient.loop();
64 | } catch (Exception e) {
65 | weChatClient.stop();
66 | throw new RuntimeException(e);
67 | }
68 |
69 | return 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/WeChatCore.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc;
2 |
3 | import com.meteor.wechatbc.impl.HttpAPI;
4 | import org.apache.logging.log4j.Logger;
5 |
6 | public interface WeChatCore {
7 |
8 | HttpAPI getHttpAPI();
9 |
10 | String getAPIVersion();
11 |
12 | Logger getLogger();
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/command/CommandExecutor.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.command;
2 |
3 | import com.meteor.wechatbc.command.sender.CommandSender;
4 |
5 | /**
6 | * 指令执行的具体委托类接口
7 | */
8 | public interface CommandExecutor {
9 |
10 | void onCommand(CommandSender commandSender,String[] args);
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/command/WeChatCommand.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.command;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class WeChatCommand {
7 | private String mainCommand;
8 | private CommandExecutor commandExecutor;
9 | public WeChatCommand(String mainCommand){
10 | this.mainCommand = mainCommand;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/command/sender/CommandSender.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.command.sender;
2 |
3 | /**
4 | * 指令的执行者
5 | */
6 | public interface CommandSender {
7 | void sendMessage(String message);
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/command/sender/ConsoleSender.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.command.sender;
2 |
3 | import com.meteor.wechatbc.impl.WeChatClient;
4 |
5 | /**
6 | * 由控制台执行
7 | */
8 | public class ConsoleSender implements CommandSender{
9 |
10 | private WeChatClient weChatClient;
11 |
12 | public ConsoleSender(WeChatClient weChatClient){
13 | this.weChatClient = weChatClient;
14 | }
15 |
16 | @Override
17 | public void sendMessage(String message) {
18 | weChatClient.getCommandManager().getLogger().info(message);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/command/sender/ContactSender.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.command.sender;
2 |
3 | import com.meteor.wechatbc.impl.HttpAPI;
4 | import com.meteor.wechatbc.Main;
5 | import com.meteor.wechatbc.entitiy.contact.Contact;
6 | import com.meteor.wechatbc.impl.WeChatClient;
7 | import lombok.AllArgsConstructor;
8 | import lombok.Getter;
9 |
10 | /**
11 | * 由联系人执行
12 | */
13 | @AllArgsConstructor
14 | public class ContactSender implements CommandSender{
15 | @Getter private Contact contact; // 指令的执行者
16 |
17 | private String formUserName; // 消息的发出地 (群聊,或私聊窗口等)
18 |
19 | // 这里的实现其实不是很优雅,有时间再改
20 | @Override
21 | public void sendMessage(String message) {
22 | WeChatClient weChatClient = Main.weChatClient;
23 | HttpAPI httpAPI = weChatClient.getWeChatCore().getHttpAPI();
24 | httpAPI.sendMessage(formUserName,message);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/config/BaseConfigurationSection.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.config;
2 |
3 | import java.util.HashMap;
4 | import java.util.List;
5 | import java.util.Map;
6 |
7 | public abstract class BaseConfigurationSection implements ConfigurationSection {
8 | protected Map data = new HashMap<>();
9 |
10 | @Override
11 | public String getString(String path) {
12 | Object value = data.get(path);
13 | return value instanceof String ? (String) value : null;
14 | }
15 |
16 | @Override
17 | public int getInt(String path) {
18 | Object value = data.get(path);
19 | return value instanceof Number ? ((Number) value).intValue() : 0;
20 | }
21 |
22 | @Override
23 | public boolean getBoolean(String path) {
24 | Object value = data.get(path);
25 | return value instanceof Boolean ? (Boolean) value : false;
26 | }
27 |
28 | @Override
29 | public double getDouble(String path) {
30 | Object value = data.get(path);
31 | return value instanceof Number ? ((Number) value).doubleValue() : 0;
32 | }
33 |
34 | @SuppressWarnings("unchecked")
35 | @Override
36 | public List> getList(String path) {
37 | Object value = data.get(path);
38 | return value instanceof List ? (List>) value : null;
39 | }
40 |
41 | @Override
42 | public List getStringList(String path) {
43 | Object value = data.get(path);
44 | return value instanceof List ? (List) value : null;
45 | }
46 |
47 | @Override
48 | public void set(String path, Object value) {
49 | data.put(path, value);
50 | }
51 |
52 | @Override
53 | public boolean contains(String path) {
54 | return data.containsKey(path);
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/config/ConfigurationSection.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.config;
2 |
3 | import java.util.List;
4 |
5 | public interface ConfigurationSection {
6 | String getString(String path);
7 | int getInt(String path);
8 | boolean getBoolean(String path);
9 | double getDouble(String path);
10 | List> getList(String path);
11 | List getStringList(String path);
12 | ConfigurationSection getConfigurationSection(String path);
13 | void set(String path, Object value);
14 | boolean contains(String path);
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/config/FileConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.config;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 | import java.io.FileWriter;
6 | import java.io.IOException;
7 | import java.util.LinkedHashMap;
8 | import java.util.Map;
9 | import org.yaml.snakeyaml.DumperOptions;
10 | import org.yaml.snakeyaml.Yaml;
11 |
12 | public abstract class FileConfiguration implements ConfigurationSection {
13 | protected Map data = new LinkedHashMap<>();
14 |
15 | public abstract void save(File file) throws IOException;
16 | public abstract void load(File file) throws IOException;
17 |
18 | @Override
19 | public String getString(String path) {
20 | Object value = data.get(path);
21 | if (value instanceof String) {
22 | return (String) value;
23 | }
24 | return null;
25 | }
26 |
27 | @Override
28 | public int getInt(String path) {
29 | Object value = data.get(path);
30 | if (value instanceof Number) {
31 | return ((Number) value).intValue();
32 | }
33 | return 0;
34 | }
35 |
36 | @Override
37 | public void set(String path, Object value) {
38 | data.put(path, value);
39 | }
40 |
41 | @Override
42 | public ConfigurationSection getConfigurationSection(String path) {
43 | String[] keys = path.split("\\.");
44 | Map current = data;
45 |
46 | for (String key : keys) {
47 | Object value = current.get(key);
48 | if (value instanceof Map) {
49 | current = (Map) value;
50 | } else {
51 | return null;
52 | }
53 | }
54 |
55 | SimpleConfigurationSection section = new SimpleConfigurationSection();
56 | section.data = current;
57 | return section;
58 | }
59 |
60 | @Override
61 | public boolean contains(String path) {
62 | return data.containsKey(path);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/config/SimpleConfigurationSection.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.config;
2 |
3 | import java.util.ArrayList;
4 | import java.util.LinkedHashMap;
5 | import java.util.List;
6 | import java.util.Map;
7 |
8 | public class SimpleConfigurationSection implements ConfigurationSection {
9 | protected Map data = new LinkedHashMap<>();
10 |
11 | @Override
12 | public String getString(String path) {
13 | Object val = data.get(path);
14 | if (val instanceof String) {
15 | return (String) val;
16 | }
17 | return null;
18 | }
19 |
20 | @Override
21 | public int getInt(String path) {
22 | Object val = data.get(path);
23 | if (val instanceof Number) {
24 | return ((Number) val).intValue();
25 | }
26 | return 0;
27 | }
28 |
29 | @Override
30 | public boolean getBoolean(String path) {
31 | Object val = data.get(path);
32 | if (val instanceof Boolean) {
33 | return (Boolean) val;
34 | }
35 | return false;
36 | }
37 |
38 | @Override
39 | public double getDouble(String path) {
40 | Object val = data.get(path);
41 | if (val instanceof Number) {
42 | return ((Number) val).doubleValue();
43 | }
44 | return 0.0;
45 | }
46 |
47 | @SuppressWarnings("unchecked")
48 | @Override
49 | public List> getList(String path) {
50 | Object val = data.get(path);
51 | if (val instanceof List) {
52 | return (List>) val;
53 | }
54 | return new ArrayList<>();
55 | }
56 |
57 | @Override
58 | public List getStringList(String path) {
59 | Object val = data.get(path);
60 | if (val instanceof List) {
61 | return (List) val;
62 | }
63 | return new ArrayList<>();
64 | }
65 |
66 | @Override
67 | public void set(String path, Object value) {
68 | data.put(path, value);
69 | }
70 |
71 | @Override
72 | public ConfigurationSection getConfigurationSection(String path) {
73 | String[] keys = path.split("\\.");
74 | Map current = data;
75 |
76 | for (String key : keys) {
77 | Object value = current.get(key);
78 | if (value instanceof Map) {
79 | current = (Map) value;
80 | } else {
81 | return null;
82 | }
83 | }
84 |
85 | SimpleConfigurationSection section = new SimpleConfigurationSection();
86 | section.data = current;
87 | return section;
88 | }
89 |
90 | @Override
91 | public boolean contains(String path) {
92 | return data.containsKey(path);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/config/YamlConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.config;
2 |
3 | import org.yaml.snakeyaml.DumperOptions;
4 | import org.yaml.snakeyaml.Yaml;
5 |
6 | import java.io.File;
7 | import java.io.FileReader;
8 | import java.io.FileWriter;
9 | import java.io.IOException;
10 |
11 | import java.util.HashMap;
12 | import java.util.Map;
13 |
14 | public class YamlConfiguration extends BaseConfigurationSection {
15 |
16 | public static YamlConfiguration loadConfiguration(File file) {
17 | YamlConfiguration config = new YamlConfiguration();
18 | Yaml yaml = new Yaml();
19 | try (FileReader reader = new FileReader(file)) {
20 | config.data = yaml.load(reader);
21 | if (config.data == null) {
22 | config.data = new HashMap<>();
23 | }
24 | } catch (IOException e) {
25 | e.printStackTrace();
26 | }
27 | return config;
28 | }
29 |
30 | public void save(File file) throws IOException {
31 | DumperOptions options = new DumperOptions();
32 | options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
33 | Yaml yaml = new Yaml(options);
34 | try (FileWriter writer = new FileWriter(file)) {
35 | yaml.dump(data, writer);
36 | }
37 | }
38 |
39 |
40 | @Override
41 | public ConfigurationSection getConfigurationSection(String path) {
42 | String[] keys = path.split("\\.");
43 | Map current = data;
44 |
45 | for (String key : keys) {
46 | Object value = current.get(key);
47 | if (value instanceof Map) {
48 | current = (Map) value;
49 | } else {
50 | return null; // 如果路径中的任何部分不是 Map,则返回 null
51 | }
52 | }
53 |
54 | SimpleConfigurationSection section = new SimpleConfigurationSection();
55 | section.data = current;
56 | return section;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/SendMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import lombok.Builder;
5 | import lombok.Data;
6 | import lombok.ToString;
7 |
8 | @Data
9 | @Builder
10 | @ToString
11 | public class SendMessage {
12 | @JSONField(name = "Type")
13 | private String type;
14 | @JSONField(name = "Content")
15 | private String content = "";
16 | @JSONField(name = "FromUserName")
17 | private String fromUserName;
18 | @JSONField(name = "ToUserName")
19 | private String toUserName;
20 | @JSONField(name = "LocalID")
21 | private String localId;
22 | @JSONField(name = "ClientMsgId")
23 | private String clientMsgId;
24 | @JSONField(name = "MediaId")
25 | private String mediaId = "";
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/contact/Contact.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.contact;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import com.meteor.wechatbc.entitiy.message.SentMessage;
5 | import com.meteor.wechatbc.impl.HttpAPI;
6 | import com.meteor.wechatbc.impl.WeChatClient;
7 | import lombok.Data;
8 | import lombok.ToString;
9 |
10 | import java.io.File;
11 | import java.io.Serializable;
12 | import java.util.List;
13 |
14 | @Data
15 | @ToString
16 | public class Contact implements Serializable {
17 |
18 | private transient WeChatClient weChatClient;
19 |
20 | @JSONField(name="Uin")
21 | private long uin;
22 |
23 | @JSONField(name="UserName")
24 | private String userName;
25 |
26 | @JSONField(name="NickName")
27 | private String nickName;
28 |
29 | @JSONField(name="HeadImgUrl")
30 | private String headImgUrl;
31 |
32 | @JSONField(name="ContactFlag")
33 | private long contactFlag;
34 |
35 | @JSONField(name="MemberCount")
36 | private long memberCount;
37 |
38 | @JSONField(name="MemberList")
39 | private List> memberList;
40 |
41 | @JSONField(name="RemarkName")
42 | private String remarkName;
43 |
44 | @JSONField(name="HideInputBarFlag")
45 | private long hideInputBarFlag;
46 |
47 | @JSONField(name="Sex")
48 | private long sex;
49 |
50 | @JSONField(name="Signature")
51 | private String signature;
52 |
53 | @JSONField(name="VerifyFlag")
54 | private long verifyFlag;
55 |
56 | @JSONField(name="OwnerUin")
57 | private long ownerUin;
58 |
59 | @JSONField(name="PYInitial")
60 | private String pyInitial;
61 |
62 | @JSONField(name="PYQuanPin")
63 | private String pyQuanPin;
64 |
65 | @JSONField(name="RemarkPYInitial")
66 | private String remarkPYInitial;
67 |
68 | @JSONField(name="RemarkPYQuanPin")
69 | private String remarkPYQuanPin;
70 |
71 | @JSONField(name="StarFriend")
72 | private long starFriend;
73 |
74 | @JSONField(name="AppAccountFlag")
75 | private long appAccountFlag;
76 |
77 | @JSONField(name="Statues")
78 | private long statues;
79 |
80 | @JSONField(name="AttrStatus")
81 | private long attrStatus;
82 |
83 | @JSONField(name="Province")
84 | private String province;
85 |
86 | @JSONField(name="City")
87 | private String city;
88 |
89 | @JSONField(name="Alias")
90 | private String alias;
91 |
92 | @JSONField(name="SnsFlag")
93 | private long snsFlag;
94 |
95 | @JSONField(name="UniFriend")
96 | private long uniFriend;
97 |
98 | @JSONField(name="DisplayName")
99 | private String displayName;
100 |
101 | @JSONField(name="ChatRoomId")
102 | private long chatRoomId;
103 |
104 | @JSONField(name="KeyWord")
105 | private String keyWord;
106 |
107 | @JSONField(name="EncryChatRoomId")
108 | private String encryChatRoomId;
109 |
110 | @JSONField(name="IsOwner")
111 | private long isOwner;
112 |
113 | protected HttpAPI httpAPI(){
114 | return weChatClient.getWeChatCore().getHttpAPI();
115 | }
116 |
117 | public SentMessage sendMessage(String message){
118 | return httpAPI().sendMessage(getUserName(),message);
119 | }
120 |
121 | public SentMessage sendImage(File file){
122 | return httpAPI().sendImage(getUserName(),file);
123 | }
124 |
125 | public SentMessage sendVideo(File file){
126 | return httpAPI().sendVideo(getUserName(),file);
127 | }
128 |
129 | /**
130 | * 判断是否为群聊
131 | * @return
132 | */
133 | public boolean isGroup(){
134 | return getUserName().startsWith("@@");
135 | }
136 |
137 | /**
138 | * 用户类型
139 | * @return
140 | */
141 | public ContactType getContactType(){
142 | return ContactType.from(this);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/contact/ContactType.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.contact;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 |
5 | public enum ContactType {
6 |
7 | GROUP("群"),FRIEND("好友"),GG("公众号"),CONTACT("用户");
8 |
9 | private String comment;
10 | ContactType(String comment){
11 | this.comment = comment;
12 | }
13 |
14 | public String getComment() {
15 | return comment;
16 | }
17 |
18 | public static ContactType from(Contact contact){
19 | if(contact.getUserName().startsWith("@@")) return GROUP;
20 | else if(contact.getUserName().startsWith("@")) return FRIEND;
21 | else if(contact.getVerifyFlag() == 8 || contact.getVerifyFlag() == 24 || contact.getVerifyFlag() == 136) return GG;
22 | return CONTACT;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/contact/GetBatchContact.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.contact;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import lombok.Data;
5 | import lombok.ToString;
6 |
7 | @Data
8 | @ToString
9 | public class GetBatchContact {
10 |
11 | @JSONField(name="UserName")
12 | private String userName;
13 |
14 | @JSONField(name="EncryChatRoomId")
15 | private String encryChatRoomId;
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/message/AppInfo.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.message;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import lombok.Data;
5 |
6 | @Data
7 | public class AppInfo {
8 | @JSONField(name = "AppID")
9 | private String appId;
10 |
11 | @JSONField(name = "Type")
12 | private Integer type;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/message/Message.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.message;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import com.meteor.wechatbc.impl.model.MsgType;
5 | import com.meteor.wechatbc.impl.model.message.VideoMessage;
6 | import lombok.Builder;
7 | import lombok.Data;
8 | import lombok.ToString;
9 | import org.jetbrains.annotations.TestOnly;
10 |
11 | @Data
12 | @ToString
13 | public class Message {
14 | @JSONField(name = "MsgId")
15 | private Long msgId;
16 |
17 | @JSONField(name = "FromUserName")
18 | private String fromUserName;
19 |
20 | @JSONField(name = "ToUserName")
21 | private String toUserName;
22 |
23 | @JSONField(name = "MsgType")
24 | private Integer msgType;
25 |
26 | @JSONField(name = "Content")
27 | private String content;
28 |
29 | @JSONField(name = "Status")
30 | private Integer status;
31 |
32 | @JSONField(name = "ImgStatus")
33 | private Integer imgStatus;
34 |
35 | @JSONField(name = "CreateTime")
36 | private Long createTime;
37 |
38 | @JSONField(name = "VoiceLength")
39 | private Integer voiceLength;
40 |
41 | @JSONField(name = "PlayLength")
42 | private Integer playLength;
43 |
44 | @JSONField(name = "FileName")
45 | private String fileName;
46 |
47 | @JSONField(name = "FileSize")
48 | private String fileSize;
49 |
50 | @JSONField(name = "MediaId")
51 | private String mediaId;
52 |
53 | @JSONField(name = "Url")
54 | private String url;
55 |
56 | @JSONField(name = "AppMsgType")
57 | private Integer appMsgType;
58 |
59 | @JSONField(name = "StatusNotifyCode")
60 | private Integer statusNotifyCode;
61 |
62 | @JSONField(name = "StatusNotifyUserName")
63 | private String statusNotifyUserName;
64 |
65 | @JSONField(name = "RecommendInfo")
66 | private RecommendInfo recommendInfo;
67 |
68 | @JSONField(name = "ForwardFlag")
69 | private Integer forwardFlag;
70 |
71 | @JSONField(name = "AppInfo")
72 | private AppInfo appInfo;
73 |
74 | @JSONField(name = "HasProductId")
75 | private Integer hasProductId;
76 |
77 | @JSONField(name = "Ticket")
78 | private String ticket;
79 |
80 | @JSONField(name = "ImgHeight")
81 | private Integer imgHeight;
82 |
83 | @JSONField(name = "ImgWidth")
84 | private Integer imgWidth;
85 |
86 | @JSONField(name = "SubMsgType")
87 | private Integer subMsgType;
88 |
89 | @JSONField(name = "NewMsgId")
90 | private Long newMsgId;
91 |
92 | @JSONField(name = "OriContent")
93 | private String oriContent;
94 |
95 | @JSONField(name = "EncryFileName")
96 | private String encryFileName;
97 |
98 | public static final String GET_AT_USER_REG = "@([^:])+:
";
99 |
100 |
101 | /**
102 | * 如果是群聊消息,该函数返回调用者
103 | */
104 | public String getSenderUserName(){
105 | if(getFromUserName().startsWith("@@")){
106 | return content.substring(0,content.indexOf(":"));
107 | }
108 | return getFromUserName();
109 | }
110 |
111 | /**
112 | * 格式化文本
113 | * @return
114 | */
115 | public String getContent() {
116 | // 群聊消息转换为纯文本
117 | String s = content.replaceAll(GET_AT_USER_REG, "");
118 | return s;
119 | }
120 |
121 | // 消息类型
122 | public MsgType getMsgType(){
123 | return MsgType.fromIdx(String.valueOf(msgType));
124 | }
125 |
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/message/RecommendInfo.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.message;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import lombok.Data;
5 |
6 | @Data
7 | public class RecommendInfo {
8 | @JSONField(name = "UserName")
9 | private String userName;
10 |
11 | @JSONField(name = "NickName")
12 | private String nickName;
13 |
14 | @JSONField(name = "QQNum")
15 | private Integer qqNum;
16 |
17 | @JSONField(name = "Province")
18 | private String province;
19 |
20 | @JSONField(name = "City")
21 | private String city;
22 |
23 | @JSONField(name = "Content")
24 | private String content;
25 |
26 | @JSONField(name = "Signature")
27 | private String signature;
28 |
29 | @JSONField(name = "Alias")
30 | private String alias;
31 |
32 | @JSONField(name = "Scene")
33 | private Integer scene;
34 |
35 | @JSONField(name = "VerifyFlag")
36 | private Integer verifyFlag;
37 |
38 | @JSONField(name = "AttrStatus")
39 | private Integer attrStatus;
40 |
41 | @JSONField(name = "Sex")
42 | private Integer sex;
43 |
44 | @JSONField(name = "Ticket")
45 | private String ticket;
46 |
47 | @JSONField(name = "OpCode")
48 | private Integer opCode;
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/message/SentMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.message;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import com.meteor.wechatbc.entitiy.SendMessage;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Data;
7 |
8 | @Data
9 | @AllArgsConstructor
10 | public class SentMessage {
11 | @JSONField(name = "Msg")
12 | private SendMessage sendMessage;
13 | private String msgId;
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/session/BaseRequest.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.session;
2 |
3 |
4 | import com.alibaba.fastjson2.annotation.JSONField;
5 | import com.meteor.wechatbc.impl.cookie.CookiePack;
6 | import lombok.Data;
7 | import lombok.ToString;
8 | import okhttp3.Cookie;
9 | import org.apache.logging.log4j.LogManager;
10 | import org.w3c.dom.Document;
11 | import org.w3c.dom.Element;
12 | import org.w3c.dom.NodeList;
13 | import org.xml.sax.SAXException;
14 |
15 | import javax.xml.parsers.DocumentBuilder;
16 | import javax.xml.parsers.DocumentBuilderFactory;
17 | import javax.xml.parsers.ParserConfigurationException;
18 | import java.io.*;
19 | import java.util.List;
20 | import java.util.stream.Collectors;
21 |
22 | /**
23 | * 公共请求
24 | * 登陆后的所有接口都需要携带该类
25 | */
26 | @Data
27 | @ToString
28 | public class BaseRequest implements Serializable {
29 | @JSONField(name = "Uin")
30 | private String uin;
31 | @JSONField(name = "Sid")
32 | private String sid;
33 | @JSONField(name = "Skey")
34 | private String skey;
35 | @JSONField(name = "DeviceID")
36 | private String deviceId;
37 | @JSONField(serialize = false)
38 | private String dataTicket;
39 | @JSONField(serialize = false)
40 | private String passTicket;
41 | @JSONField(serialize = false)
42 | private String authTicket;
43 |
44 | @JSONField(serialize = false)
45 |
46 | transient private List initCookie; // 该类不能进行序列化
47 |
48 | private List cookiePacks; // 代理了cookie类来实现序列化
49 |
50 | public void setInitCookie(List initCookie) {
51 | this.initCookie = initCookie;
52 | this.cookiePacks = initCookie.stream().map(cookie -> new CookiePack(cookie))
53 | .collect(Collectors.toList());
54 | }
55 |
56 | public List getInitCookie() {
57 | if(initCookie == null){
58 | this.initCookie = cookiePacks.stream().map(cookiePack -> cookiePack.getCookie()).collect(Collectors.toList());
59 | }
60 | return initCookie;
61 | }
62 |
63 | // 解析xml
64 | public BaseRequest(String xmlData){
65 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
66 | DocumentBuilder builder = null;
67 | try {
68 | builder = factory.newDocumentBuilder();
69 | Document document = builder.parse(new ByteArrayInputStream(xmlData.getBytes()));
70 |
71 | Element errorElement = (Element) document.getElementsByTagName("error").item(0);
72 |
73 | String ret = errorElement.getElementsByTagName("ret").item(0).getTextContent();
74 | if(ret.equalsIgnoreCase("0")){
75 | NodeList skeyList = document.getElementsByTagName("skey");
76 | if (skeyList.getLength() > 0) {
77 | Element skeyElement = (Element) skeyList.item(0);
78 | this.skey = skeyElement.getTextContent();;
79 | }
80 | NodeList wxsidList = document.getElementsByTagName("wxsid");
81 | if (wxsidList.getLength() > 0) {
82 | Element wxsidElement = (Element) wxsidList.item(0);
83 | this.sid = wxsidElement.getTextContent();
84 | }
85 | NodeList wxuniList = document.getElementsByTagName("wxuin");
86 | if (wxsidList.getLength() > 0) {
87 | Element wxuniElement = (Element) wxuniList.item(0);
88 | this.uin = wxuniElement.getTextContent();
89 | }
90 | NodeList passTicketList = document.getElementsByTagName("pass_ticket");
91 | if (passTicketList.getLength() > 0) {
92 | Element passTicketElement = (Element) passTicketList.item(0);
93 | this.passTicket = passTicketElement.getTextContent();
94 | }
95 | }else {
96 | String message = errorElement.getElementsByTagName("message").item(0).getTextContent();
97 | LogManager.getLogger("BASE-REQUEST").error("登录失败:" + message + " CODE: " + ret);
98 | throw new RuntimeException();
99 | }
100 | } catch (ParserConfigurationException e) {
101 | throw new RuntimeException(e);
102 | } catch (IOException e) {
103 | throw new RuntimeException(e);
104 | } catch (SAXException e) {
105 | throw new RuntimeException(e);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/session/Skey.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.session;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 | import lombok.Data;
5 |
6 | import java.io.Serializable;
7 |
8 | @Data
9 | public class Skey implements Serializable {
10 |
11 | @JSONField(name = "Key")
12 | private int Key;
13 | @JSONField(name = "Val")
14 | private long val;
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/session/SyncKey.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.session;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 | import lombok.Data;
5 |
6 | import java.io.Serializable;
7 | import java.util.Arrays;
8 | import java.util.stream.Collectors;
9 |
10 | /**
11 | * 同步消息使用
12 | * 这里的值每次使用过后都会更新
13 | */
14 | @Data
15 | public class SyncKey implements Serializable {
16 | @JSONField(name = "Count")
17 | private int count;
18 | @JSONField(name = "List")
19 | private Skey[] keys;
20 |
21 | /**
22 | * 在synccheck接口需要用到格式化后的字符串作为参数
23 | * @return
24 | */
25 | @Override
26 | public String toString(){
27 | return Arrays.stream(keys)
28 | .map(skey -> String.format("%s_%s",skey.getKey(),skey.getVal()))
29 | .collect(Collectors.joining("|"));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/synccheck/SyncCheckResponse.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.synccheck;
2 |
3 | import lombok.Data;
4 |
5 | /**
6 | * 轮询查看新消息通知接口的响应信息
7 | */
8 | @Data
9 | public class SyncCheckResponse {
10 | private SyncCheckRetcode syncCheckRetcode;
11 | private SyncCheckSelector syncCheckSelector;
12 |
13 | public SyncCheckResponse(String retcode,String selector){
14 | this.syncCheckRetcode = SyncCheckRetcode.form(retcode);
15 | this.syncCheckSelector = SyncCheckSelector.form(selector);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/synccheck/SyncCheckRetcode.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.synccheck;
2 |
3 | import lombok.Getter;
4 |
5 | public enum SyncCheckRetcode {
6 | NORMAL("0", "正常"),
7 | TICK_ERROR("-14", "ticket错误"),
8 | PARAM_ERROR("1", "传入参数错误"),
9 | LOGIN_OUT("1100", "已登出微信"),
10 | NOT_LOGIN("1101", "未登录微信"),
11 | COOKIE_ERROR("1102","cookie值无效"),
12 | LOGIN_FAIL("1203","当前登录环境异常"),
13 | OFTEN("1205","操作频繁");
14 |
15 | @Getter private String code;
16 | @Getter private String message;
17 | SyncCheckRetcode(String code,String message){
18 | this.code = code;
19 | this.message = message;
20 | }
21 | public static SyncCheckRetcode form(String code){
22 | for (SyncCheckRetcode value : SyncCheckRetcode.values()) {
23 | if(value.code.equalsIgnoreCase(code)) return value;
24 | }
25 | return NORMAL;
26 | }
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/entitiy/synccheck/SyncCheckSelector.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.entitiy.synccheck;
2 |
3 | import lombok.Getter;
4 |
5 | /**
6 | * 异步消息更新类型
7 | */
8 | public enum SyncCheckSelector {
9 | NORMAL("0", "正常"),
10 | NEW_MSG("2", "有新消息"),
11 | MOD_CONTACT("4", "昵称修改或备注"),
12 | ADD_OR_DEL_CONTACT("6", "删除或者新增的好友信息"),
13 | ENTER_OR_LEAVE_CHAT("7", "进入或离开聊天界面");
14 |
15 | @Getter private String code;
16 | @Getter private String message;
17 | SyncCheckSelector(String code,String message){
18 | this.code = code;
19 | this.message = message;
20 | }
21 | public static SyncCheckSelector form(String code){
22 | for (SyncCheckSelector value : SyncCheckSelector.values()) {
23 | if(value.code.equalsIgnoreCase(code)) return value;
24 | }
25 | return NORMAL;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/event/Event.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.event;
2 |
3 | import com.meteor.wechatbc.impl.event.EventManager;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 |
7 | /**
8 | * 所有事件的基类
9 | */
10 | public class Event {
11 |
12 | @Getter @Setter
13 | private EventManager eventManager;
14 |
15 | /**
16 | * 只允许对子类进行构造
17 | */
18 | protected Event(){
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/event/EventBus.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.event;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | import java.lang.reflect.InvocationTargetException;
7 | import java.lang.reflect.Method;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 | import java.util.Map;
11 | import java.util.concurrent.ConcurrentHashMap;
12 |
13 | public class EventBus {
14 |
15 | @Data
16 | @AllArgsConstructor
17 | public static class EventListener{
18 | private Object target;
19 | private Method method;
20 | private ClassLoader classLoader;
21 | }
22 |
23 |
24 | private final Map, List> listeners = new ConcurrentHashMap<>();
25 |
26 | /**
27 | * 注册监听器类
28 | * @param obj
29 | */
30 | public void register(Object obj) {
31 | Method[] methods = obj.getClass().getDeclaredMethods();
32 | ClassLoader classLoader = obj.getClass().getClassLoader(); // 获取类加载器
33 | for (Method method : methods) {
34 | if(method.isAnnotationPresent(EventHandler.class) && method.getParameterCount() == 1){
35 | Class> eventType = method.getParameterTypes()[0];
36 | listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(new EventListener(obj, method, classLoader));
37 | }
38 | }
39 | }
40 |
41 | /**
42 | * 卸载监听器类
43 | * @param obj
44 | */
45 | public void unRegister(Object obj){
46 | listeners.values().forEach(listeners -> listeners.removeIf(listener -> listener.getTarget() == obj));
47 | }
48 |
49 | /**
50 | * 调用事件
51 | */
52 | public void post(Object event) {
53 | List eventListeners = listeners.get(event.getClass());
54 | if(eventListeners != null) {
55 | ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
56 | try {
57 | for (EventListener eventListener : eventListeners) {
58 | ClassLoader listenerClassLoader = eventListener.getClassLoader();
59 | Thread.currentThread().setContextClassLoader(listenerClassLoader); // 切换类加载器
60 | Method method = eventListener.getMethod();
61 | boolean accessible = method.isAccessible();
62 | method.setAccessible(true); // 使私有方法可访问
63 | method.invoke(eventListener.getTarget(), event);
64 | method.setAccessible(accessible);
65 | }
66 | } catch (IllegalAccessException | InvocationTargetException e) {
67 | e.printStackTrace();
68 | throw new RuntimeException(e);
69 | } finally {
70 | Thread.currentThread().setContextClassLoader(originalClassLoader); // 恢复原类加载器
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/event/EventHandler.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.event;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target(ElementType.METHOD)
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface EventHandler {
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/DefaultPlugin.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl;
2 |
3 | import com.meteor.wechatbc.impl.plugin.BasePlugin;
4 |
5 | /**
6 | * 一个插件标识,用于注册一些预设的监听器,指令
7 | */
8 | public class DefaultPlugin extends BasePlugin {
9 | @Override
10 | public void onLoad() {
11 | }
12 |
13 | @Override
14 | public void onEnable() {
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/HttpAPI.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl;
2 |
3 | import com.alibaba.fastjson2.JSONObject;
4 | import com.meteor.wechatbc.entitiy.contact.GetBatchContact;
5 | import com.meteor.wechatbc.entitiy.message.SentMessage;
6 | import com.meteor.wechatbc.entitiy.synccheck.SyncCheckResponse;
7 |
8 | import java.io.File;
9 | import java.util.List;
10 |
11 | public interface HttpAPI {
12 |
13 | /**
14 | * 初始化
15 | */
16 | void init();
17 |
18 | /**
19 | * 初始化微信接口
20 | */
21 | void initWeChat();
22 |
23 | /**
24 | * 检查新消息
25 | */
26 | SyncCheckResponse syncCheck();
27 |
28 | /**
29 | * 获取最新消息
30 | */
31 | JSONObject getMessage();
32 |
33 | /**
34 | * 取得联系人列表
35 | */
36 | JSONObject getContact();
37 |
38 | /**
39 | * 批量获取联系人详情,人或群均可。获取群详情主要是获取群内联系人列表。获取人详情主要是获取群内的某个人的详细信息
40 | *
41 | * @param queryContactList
42 | * @return
43 | */
44 | JSONObject batchGetContactDetail(List queryContactList);
45 |
46 | /**
47 | * 发送消息
48 | */
49 | SentMessage sendMessage(String toUserName, String content);
50 |
51 | /**
52 | * 获得消息图片
53 | */
54 | byte[] getMsgImage(String msgId);
55 |
56 | void revoke(SentMessage sentMessage);
57 |
58 | /**
59 | * 发送图片
60 | *
61 | * @return
62 | */
63 | SentMessage sendImage(String toUserName, File file);
64 |
65 | /**
66 | * 发送视频
67 | *
68 | * @return
69 | */
70 | SentMessage sendVideo(String toUserName, File file);
71 |
72 | /**
73 | * 获取用户头像
74 | *
75 | * @return
76 | */
77 | File getIcon(String userName);
78 |
79 | /**
80 | * 获取视频消息的响应
81 | */
82 | byte[] getVideo(long msgId);
83 |
84 | /**
85 | * 获取语音消息
86 | * @param msgId
87 | * @return
88 | */
89 | byte[] getVoice(long msgId);
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/HttpAPIImpl.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl;
2 |
3 | import com.alibaba.fastjson2.JSON;
4 | import com.alibaba.fastjson2.JSONObject;
5 | import com.alibaba.fastjson2.JSONReader;
6 | import com.meteor.wechatbc.entitiy.SendMessage;
7 | import com.meteor.wechatbc.entitiy.contact.Contact;
8 | import com.meteor.wechatbc.entitiy.contact.GetBatchContact;
9 | import com.meteor.wechatbc.entitiy.message.SentMessage;
10 | import com.meteor.wechatbc.entitiy.session.BaseRequest;
11 | import com.meteor.wechatbc.entitiy.synccheck.SyncCheckResponse;
12 | import com.meteor.wechatbc.impl.cookie.WeChatCookie;
13 | import com.meteor.wechatbc.impl.fileupload.FileChunkUploader;
14 | import com.meteor.wechatbc.impl.fileupload.model.UploadMediaRequest;
15 | import com.meteor.wechatbc.impl.fileupload.model.UploadResponse;
16 | import com.meteor.wechatbc.impl.interceptor.WeChatInterceptor;
17 | import com.meteor.wechatbc.impl.model.MsgType;
18 | import com.meteor.wechatbc.impl.model.Session;
19 | import com.meteor.wechatbc.impl.model.WxInitInfo;
20 | import com.meteor.wechatbc.util.HttpUrlHelper;
21 | import com.meteor.wechatbc.util.URL;
22 | import lombok.Getter;
23 | import okhttp3.*;
24 |
25 | import javax.imageio.ImageIO;
26 | import java.awt.image.BufferedImage;
27 | import java.io.ByteArrayInputStream;
28 | import java.io.File;
29 | import java.io.IOException;
30 | import java.util.ArrayList;
31 | import java.util.List;
32 | import java.util.concurrent.TimeUnit;
33 |
34 | /**
35 | * 微信接口的实现
36 | */
37 | public class HttpAPIImpl implements HttpAPI {
38 |
39 |
40 | private WeChatClient weChatClient;
41 |
42 | @Getter private OkHttpClient okHttpClient;
43 |
44 | private WeChatInterceptor weChatInterceptor;
45 |
46 | private final MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
47 |
48 | @Getter private final Request BASE_REQUEST = new Request.Builder().addHeader("Content-Type","application/json; charset=UTF-8").url(URL.BASE_URL).build();
49 |
50 | private WeChatCookie weChatCookie;
51 |
52 | public HttpAPIImpl(WeChatClient weChatClient){
53 | this.weChatClient = weChatClient;
54 | }
55 |
56 | public void init(){
57 | this.weChatCookie = new WeChatCookie(weChatClient.getWeChatCore().getSession().getBaseRequest().getInitCookie());
58 | this.weChatInterceptor = new WeChatInterceptor(weChatClient.getWeChatCore());
59 | this.okHttpClient = new OkHttpClient.Builder()
60 | .cookieJar(this.weChatCookie) // cookie处理
61 | .readTimeout(120, TimeUnit.SECONDS)
62 | .connectTimeout(120,TimeUnit.SECONDS)
63 | .addInterceptor(this.weChatInterceptor) // 拦截器
64 | .build();
65 | }
66 |
67 | @Override
68 | public void initWeChat() {
69 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
70 | .encodedPath(URL.WXINIT)
71 | .addQueryParameter("_",String.valueOf(System.currentTimeMillis()))
72 | .build();
73 |
74 | Request request = BASE_REQUEST.newBuilder().url(httpUrl).post(RequestBody.create(mediaType,JSON.toJSONString(new JSONObject()))).build();
75 |
76 | try(
77 | Response response = okHttpClient.newCall(request).execute()
78 | ) {
79 | String responseToString = response.body().string();
80 | Session session = weChatClient.getWeChatCore().getSession();
81 | WxInitInfo wxInitInfo = JSON.parseObject(responseToString, WxInitInfo.class);
82 | session.setWxInitInfo(wxInitInfo);
83 | session.setCheckSyncKey(wxInitInfo.getSyncKey());
84 | session.setSyncKey(wxInitInfo.getSyncKey());
85 | Contact user = wxInitInfo.getUser();
86 | weChatClient.getLogger().debug("已初始化微信信息:");
87 | weChatClient.getLogger().debug("用户信息:");
88 | weChatClient.getLogger().debug(user.toString());
89 | } catch (IOException e) {
90 | throw new RuntimeException(e);
91 | }
92 | }
93 |
94 | @Override
95 | public SyncCheckResponse syncCheck() {
96 | Session session = weChatClient.getWeChatCore().getSession();
97 | BaseRequest baseRequest = session.getBaseRequest();
98 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
99 | .encodedPath(URL.SYNCCHECK)
100 | .addQueryParameter("r",String.valueOf(System.currentTimeMillis()))
101 | .addQueryParameter("skey",baseRequest.getSkey())
102 | .addQueryParameter("sid",baseRequest.getSid())
103 | .addQueryParameter("uin",baseRequest.getUin())
104 | .addQueryParameter("deviceid",baseRequest.getDeviceId())
105 | .addQueryParameter("_",String.valueOf(System.currentTimeMillis()))
106 | .addQueryParameter("synckey",weChatClient.getWeChatCore().getSession().getCheckSyncKey().toString())
107 | .build();
108 | Request request = BASE_REQUEST.newBuilder().url(httpUrl).get().build();
109 | try (
110 | Response response = okHttpClient.newCall(request).execute();
111 | ){
112 | String body = response.body().string();
113 | body = HttpUrlHelper.getValueByKey(body,"window.synccheck");
114 | JSONReader reader = JSONReader.of(body);
115 | reader.getContext().config(JSONReader.Feature.AllowUnQuotedFieldNames, true); // 允许未加引号的字段名
116 | JSONObject jsonObject = reader.read(JSONObject.class);
117 | return new SyncCheckResponse(jsonObject.getString("retcode"),jsonObject.getString("selector"));
118 | } catch (IOException e) {
119 | e.printStackTrace();
120 | throw new RuntimeException(e);
121 | }
122 | }
123 |
124 | @Override
125 | public JSONObject getMessage() {
126 | Session session = weChatClient.getWeChatCore().getSession();
127 | BaseRequest baseRequest = session.getBaseRequest();
128 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
129 | .encodedPath(URL.WEBWXSYNC)
130 | .addQueryParameter("sid",baseRequest.getSid())
131 | .addQueryParameter("skey",baseRequest.getSkey())
132 | .addQueryParameter("pass_ticket",baseRequest.getPassTicket())
133 | .addQueryParameter("rr",String.valueOf(System.currentTimeMillis()))
134 | .build();
135 | Request request = BASE_REQUEST.newBuilder().url(httpUrl)
136 | .post(RequestBody.create(mediaType,JSON.toJSONString(session)))
137 | .build();
138 | try(
139 | Response response = okHttpClient.newCall(request).execute()
140 | ) {
141 | String body = response.body().string();
142 | JSONObject jsonObject = JSONObject.parseObject(body);
143 | return jsonObject;
144 | } catch (IOException e) {
145 | weChatClient.getLogger().info(e.getMessage());
146 | throw new RuntimeException(e);
147 | }
148 | }
149 |
150 | @Override
151 | public JSONObject getContact() {
152 | Session session = weChatClient.getWeChatCore().getSession();
153 | BaseRequest baseRequest = session.getBaseRequest();
154 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
155 | .encodedPath(URL.GET_CONTACT)
156 | .addQueryParameter("skey",baseRequest.getSkey())
157 | .addQueryParameter("pass_ticket",baseRequest.getPassTicket())
158 | .addQueryParameter("rr",String.valueOf(System.currentTimeMillis()))
159 | .build();
160 | Request request = BASE_REQUEST.newBuilder().url(httpUrl)
161 | .post(RequestBody.create(mediaType,JSONObject.toJSONString(new JSONObject())))
162 | .build();
163 | try(
164 | Response response = okHttpClient.newCall(request).execute();
165 | ) {
166 | String body = response.body().string();
167 | return JSON.parseObject(body);
168 | } catch (IOException e) {
169 | throw new RuntimeException(e);
170 | }
171 | }
172 |
173 |
174 | @Override
175 | public JSONObject batchGetContactDetail(List queryContactList) {
176 | if (queryContactList == null || queryContactList.isEmpty()) {
177 | queryContactList = new ArrayList<>();
178 | }
179 |
180 | Integer count = queryContactList.size();
181 | Session session = weChatClient.getWeChatCore().getSession();
182 | BaseRequest baseRequest = session.getBaseRequest();
183 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
184 | .encodedPath(URL.BATCH_GET_CONTACT)
185 | .addQueryParameter("skey", baseRequest.getSkey())
186 | .addQueryParameter("pass_ticket", baseRequest.getPassTicket())
187 | .addQueryParameter("rr", String.valueOf(System.currentTimeMillis()))
188 | .addQueryParameter("type", "ex")
189 | .build();
190 |
191 | JSONObject jsonObject = new JSONObject();
192 | jsonObject.put("Count", count);
193 | jsonObject.put("List", queryContactList);
194 | Request request = BASE_REQUEST.newBuilder().url(httpUrl)
195 | .post(RequestBody.create(mediaType, jsonObject.toString()))
196 | .build();
197 | try (
198 | Response response = okHttpClient.newCall(request).execute();
199 | ) {
200 | String body = response.body().string();
201 | return JSON.parseObject(body);
202 | } catch (IOException e) {
203 | throw new RuntimeException(e);
204 | }
205 | }
206 |
207 | @Override
208 | public SentMessage sendMessage(String toUserName, String content) {
209 | Session session = weChatClient.getWeChatCore().getSession();
210 | String s = HttpUrlHelper.generateTimestampWithRandom();
211 | SendMessage sendMessage = SendMessage.builder()
212 | .fromUserName(session.getWxInitInfo().getUser().getUserName())
213 | .localId(s)
214 | .clientMsgId(s)
215 | .content(content)
216 | .type(MsgType.TextMsg.getIdx())
217 | .toUserName(toUserName)
218 | .build();
219 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
220 | .encodedPath(URL.SEND_MESSAGE)
221 | .addQueryParameter("pass_ticket","pass_ticket")
222 | .addQueryParameter("lang","zh_CN")
223 | .build();
224 | JSONObject jsonObject = new JSONObject();
225 | jsonObject.put("Msg",sendMessage);
226 | jsonObject.put("Scene",0);
227 | Request request = BASE_REQUEST.newBuilder().url(httpUrl)
228 | .post(RequestBody.create(mediaType,jsonObject.toString()))
229 | .build();
230 | try(Response response = okHttpClient.newCall(request).execute()) {
231 | String body = response.body().string();
232 | jsonObject = JSON.parseObject(body);
233 | return new SentMessage(sendMessage,jsonObject.getString("MsgID"));
234 | } catch (IOException e) {
235 | throw new RuntimeException(e);
236 | }
237 | }
238 |
239 | @Override
240 | public byte[] getMsgImage(String msgId) {
241 | Session session = weChatClient.getWeChatCore().getSession();
242 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
243 | .encodedPath(URL.GET_MSG_IMG)
244 | .addQueryParameter("MsgID",msgId)
245 | .addQueryParameter("skey",session.getBaseRequest().getSkey())
246 | .build();
247 | Request request = BASE_REQUEST.newBuilder().url(httpUrl)
248 | .get()
249 | .build();
250 | try(Response response = okHttpClient.newCall(request).execute()) {
251 | return response.body().bytes();
252 | } catch (IOException e) {
253 | throw new RuntimeException(e);
254 | }
255 | }
256 |
257 | private SentMessage sendVideo(SendMessage sendMessage){
258 |
259 | JSONObject jsonObject = new JSONObject();
260 | jsonObject.put("Msg",sendMessage);
261 | jsonObject.put("Scene",0);
262 |
263 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
264 | .encodedPath(URL.SEND_VIDEO)
265 | .addQueryParameter("f","json")
266 | .addQueryParameter("pass_ticket","pass_ticket")
267 | .addQueryParameter("lang","zh_CN")
268 | .addQueryParameter("fun","async")
269 | .build();
270 |
271 | Request request = BASE_REQUEST.newBuilder()
272 | .url(httpUrl)
273 | .post(RequestBody.create(mediaType,jsonObject.toString()))
274 | .build();
275 | try(Response response = okHttpClient.newCall(request).execute()) {
276 | jsonObject = JSON.parseObject(response.body().string());
277 | return new SentMessage(sendMessage,jsonObject.getString("MsgID"));
278 | } catch (IOException e) {
279 | throw new RuntimeException(e);
280 | }
281 | }
282 |
283 | private SentMessage sendImage(SendMessage sendMessage){
284 | Session session = weChatClient.getWeChatCore().getSession();
285 |
286 | BaseRequest baseRequest = session.getBaseRequest();
287 |
288 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
289 | .encodedPath(URL.SEND_IMAGE)
290 | .addQueryParameter("pass_ticket",baseRequest.getPassTicket())
291 | .addQueryParameter("lang","zh_CN")
292 | .addQueryParameter("fun","async")
293 | .addQueryParameter("f","json")
294 | .build();
295 |
296 | JSONObject jsonObject = new JSONObject();
297 | jsonObject.put("Msg",sendMessage);
298 | jsonObject.put("Scene",0);
299 |
300 | weChatClient.getLogger().debug( jsonObject.toString());
301 | Request request = BASE_REQUEST.newBuilder().url(httpUrl)
302 | .post(RequestBody.create(mediaType,jsonObject.toString()))
303 | .build();
304 |
305 | try(Response response = okHttpClient.newCall(request).execute()) {
306 | jsonObject = JSON.parseObject(response.body().string());
307 | return new SentMessage(sendMessage,jsonObject.getString("MsgID"));
308 | } catch (IOException e) {
309 | throw new RuntimeException(e);
310 | }
311 | }
312 |
313 | /**
314 | * 撤回消息
315 | */
316 | @Override
317 | public void revoke(SentMessage sentMessage){
318 | JSONObject jsonObject = new JSONObject();
319 | jsonObject.put("ClientMsgId",String.valueOf(sentMessage.getSendMessage().getClientMsgId()));
320 | jsonObject.put("SvrMsgId",String.valueOf(sentMessage.getMsgId()));
321 | jsonObject.put("ToUserName",sentMessage.getSendMessage().getToUserName());
322 |
323 | HttpUrl httpUrl = URL.BASE_URL.newBuilder().encodedPath(URL.REVOKE).build();
324 |
325 | Request request = BASE_REQUEST.newBuilder()
326 | .url(httpUrl)
327 | .post(RequestBody.create(mediaType,jsonObject.toString()))
328 | .build();
329 |
330 | try(Response response = okHttpClient.newCall(request).execute()) {
331 | } catch (IOException e) {
332 | throw new RuntimeException(e);
333 | }
334 | }
335 |
336 | /**
337 | * 发送图片
338 | *
339 | * @param toUserName
340 | * @return
341 | */
342 | @Override
343 | public SentMessage sendImage(String toUserName, File file) {
344 |
345 | // 尝试上传图片
346 |
347 | UploadMediaRequest uploadMediaRequest = UploadMediaRequest.builder()
348 | .toUserName(toUserName).build();
349 |
350 | UploadResponse uploadResponse = FileChunkUploader.INSTANCE.upload(file, uploadMediaRequest);
351 |
352 | Session session = weChatClient.getWeChatCore().getSession();
353 |
354 | // 如果传输成功
355 | if(uploadResponse.isFull()){
356 | String s = HttpUrlHelper.generateTimestampWithRandom();
357 | SendMessage sendMessage = SendMessage.builder()
358 | .fromUserName(session.getWxInitInfo().getUser().getUserName())
359 | .localId(s)
360 | .clientMsgId(s)
361 | .content("")
362 | .type(MsgType.ImageMsg.getIdx())
363 | .toUserName(toUserName)
364 | .mediaId(uploadResponse.getMediaId())
365 | .build();
366 | return sendImage(sendMessage);
367 | }
368 | return null;
369 | }
370 |
371 | @Override
372 | public SentMessage sendVideo(String toUserName, File file) {
373 |
374 | UploadMediaRequest uploadMediaRequest = UploadMediaRequest.builder()
375 | .toUserName(toUserName).build();
376 |
377 | UploadResponse uploadResponse = FileChunkUploader.INSTANCE.upload(file, uploadMediaRequest);
378 |
379 | Session session = weChatClient.getWeChatCore().getSession();
380 |
381 | // 如果传输成功
382 | if(uploadResponse.isFull()){
383 | String s = HttpUrlHelper.generateTimestampWithRandom();
384 |
385 | SendMessage sendMessage = SendMessage.builder()
386 | .fromUserName(session.getWxInitInfo().getUser().getUserName())
387 | .localId(s)
388 | .clientMsgId(s)
389 | .content("")
390 | .type(MsgType.VideoMsg.getIdx())
391 | .toUserName(toUserName)
392 | .mediaId(uploadResponse.getMediaId())
393 | .build();
394 |
395 | // 发送视频消息
396 | return sendVideo(sendMessage);
397 | }
398 | return null;
399 | }
400 |
401 | @Override
402 | public File getIcon(String userName) {
403 |
404 | File file = new File(weChatClient.getDataFolder(),"img/icon/"+userName+".jpg");
405 | if(file.exists()) return file;
406 |
407 | Contact contact = weChatClient.getContactManager().getContactCache().get(userName);
408 | Session session = weChatClient.getWeChatCore().getSession();
409 | if(contact.getHeadImgUrl()!=null){
410 | HttpUrl ur = URL.BASE_URL.newBuilder().encodedPath(URL.GET_ICON)
411 | .addQueryParameter("username",userName)
412 | .addQueryParameter("skey",session.getBaseRequest().getSkey())
413 | .addQueryParameter("type","big")
414 | .addQueryParameter("chatroomid",contact.getEncryChatRoomId())
415 | .addQueryParameter("seq","0")
416 | .build();
417 |
418 | Request request = BASE_REQUEST.newBuilder()
419 | .url(ur).get().build();
420 |
421 | try(Response response = okHttpClient.newCall(request).execute()){
422 | BufferedImage bufferedImage = convertHexToBufferedImage(response.body().bytes());
423 | saveImage(bufferedImage,new File(weChatClient.getDataFolder(),"img/icon/"+userName+".jpg"));
424 | return file;
425 | } catch (IOException e) {
426 | throw new RuntimeException(e);
427 | }
428 |
429 | }
430 |
431 | return null;
432 | }
433 |
434 | @Override
435 | public byte[] getVideo(long msgId) {
436 | Session session = weChatClient.getWeChatCore().getSession();
437 | HttpUrl url = URL.BASE_URL.newBuilder().encodedPath(URL.GET_VIDEO)
438 | .addQueryParameter("msgid",String.valueOf(msgId))
439 | .addQueryParameter("skey",session.getBaseRequest().getSkey())
440 | .build();
441 |
442 | Request request = BASE_REQUEST.newBuilder()
443 | .url(url)
444 | .addHeader("Referer",url.toString())
445 | .addHeader("Range","bytes=0-")
446 | .get()
447 | .build();
448 |
449 | try(Response response = okHttpClient.newCall(request).execute()) {
450 | return response.body().bytes();
451 | } catch (IOException e) {
452 | throw new RuntimeException(e);
453 | }
454 | }
455 |
456 | @Override
457 | public byte[] getVoice(long msgId){
458 | Session session = weChatClient.getWeChatCore().getSession();
459 | HttpUrl url = URL.BASE_URL.newBuilder().encodedPath(URL.GET_VOICE)
460 | .addQueryParameter("msgid",String.valueOf(msgId))
461 | .addQueryParameter("skey",session.getBaseRequest().getSkey())
462 | .build();
463 |
464 | Request request = BASE_REQUEST.newBuilder()
465 | .url(url)
466 | .addHeader("Referer",url.toString())
467 | .addHeader("Range","bytes=0-")
468 | .get()
469 | .build();
470 |
471 | try(Response response = okHttpClient.newCall(request).execute()) {
472 | return response.body().bytes();
473 | } catch (IOException e) {
474 | throw new RuntimeException(e);
475 | }
476 | }
477 |
478 | public void saveImage(BufferedImage bufferedImage,File file) {
479 | try {
480 | ImageIO.write(bufferedImage, "PNG", file);
481 | } catch (IOException e) {
482 | e.printStackTrace();
483 | }
484 | }
485 |
486 | public BufferedImage convertHexToBufferedImage(byte[] bytes) {
487 | try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes)) {
488 | return ImageIO.read(bis);
489 | } catch (IOException e) {
490 | e.printStackTrace();
491 | return null;
492 | }
493 | }
494 |
495 | }
496 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/WeChatClient.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl;
2 |
3 | import com.meteor.wechatbc.entitiy.contact.Contact;
4 | import com.meteor.wechatbc.entitiy.session.BaseRequest;
5 | import com.meteor.wechatbc.impl.command.CommandManager;
6 | import com.meteor.wechatbc.impl.console.Console;
7 | import com.meteor.wechatbc.impl.contact.ContactManager;
8 | import com.meteor.wechatbc.impl.event.EventManager;
9 | import com.meteor.wechatbc.impl.fileupload.FileChunkUploader;
10 | import com.meteor.wechatbc.impl.plugin.PluginManager;
11 | import com.meteor.wechatbc.impl.scheduler.SchedulerImpl;
12 | import com.meteor.wechatbc.impl.synccheck.SyncCheckRunnable;
13 | import com.meteor.wechatbc.launch.login.PrintQRCodeCallBack;
14 | import com.meteor.wechatbc.launch.login.WeChatLogin;
15 | import com.meteor.wechatbc.scheduler.Scheduler;
16 | import lombok.Getter;
17 | import lombok.NoArgsConstructor;
18 | import lombok.Setter;
19 | import org.apache.logging.log4j.LogManager;
20 | import org.apache.logging.log4j.Logger;
21 |
22 | import java.io.File;
23 | import java.io.IOException;
24 |
25 | /**
26 | * 客户端
27 | */
28 | @NoArgsConstructor
29 | public class WeChatClient {
30 | @Getter private Logger logger;
31 |
32 | @Getter @Setter private WeChatCoreImpl weChatCore;
33 | @Getter private SyncCheckRunnable syncCheckRunnable;
34 | @Getter private EventManager eventManager;
35 |
36 | @Getter private ContactManager contactManager;
37 |
38 | @Getter private PluginManager pluginManager;
39 |
40 | @Getter private CommandManager commandManager;
41 |
42 | @Getter private Scheduler scheduler;
43 |
44 | public WeChatClient(Logger logger){
45 | this.logger = logger;
46 | }
47 |
48 | public boolean initWeChatCore(BaseRequest baseRequest) {
49 | try {
50 | this.weChatCore = new WeChatCoreImpl(this,
51 | baseRequest);
52 | this.weChatCore.getHttpAPI().init();
53 | return true;
54 | }catch (Exception e){
55 | return false;
56 | }
57 | }
58 |
59 | /**
60 | * 一些必要的目录
61 | */
62 | public void mkDirs(){
63 | File dataFolder = getDataFolder();
64 | File iconFolder = new File(dataFolder,"img/icon");
65 | File voiceFolder = new File(dataFolder, "voice");
66 | if(!iconFolder.exists()) iconFolder.mkdirs();
67 | if(!voiceFolder.exists()) voiceFolder.mkdirs();
68 | }
69 |
70 |
71 |
72 | /**
73 | * 启动
74 | */
75 | public void start(){
76 | this.weChatCore.getHttpAPI().initWeChat();
77 | this.syncCheckRunnable = new SyncCheckRunnable(this);
78 | this.contactManager = new ContactManager(this);
79 | this.eventManager = new EventManager(this);
80 | this.commandManager = new CommandManager();
81 | this.scheduler = new SchedulerImpl();
82 |
83 | // 初始化文件上传服务
84 | FileChunkUploader.init(this);
85 | this.mkDirs();
86 |
87 | this.initPluginManager();
88 |
89 | Contact user = this.weChatCore.getSession().getWxInitInfo().getUser();
90 |
91 | logger = LogManager.getLogger(String.format("%s(%s)",user.getNickName(),user.getUin()));
92 | }
93 |
94 | public void initPluginManager(){
95 | this.pluginManager = new PluginManager(this);
96 | }
97 |
98 | private Console console;
99 |
100 | /**
101 | * 如果需要控制台的话
102 | * 调用该方法挂起
103 | */
104 | public void loop(){
105 | getLogger().info("启动控制台...");
106 | weChatCore.getHttpAPI().syncCheck();
107 | try {
108 | (this.console = new Console(this)).start();
109 | } catch (IOException e) {
110 | e.printStackTrace();
111 | logger.error("因为一些错误,控制台停止运行了");
112 | }
113 | }
114 |
115 | private WeChatLogin weChatLogin = new WeChatLogin(LogManager.getLogger("WECHAT_LOGIN"));
116 |
117 |
118 | /**
119 | * 登录
120 | */
121 | public void login(PrintQRCodeCallBack printQRCodeCallBack){
122 | String loginUUID = weChatLogin.getLoginUUID();
123 |
124 | printQRCodeCallBack.print(loginUUID);
125 |
126 | weChatLogin.waitLogin(loginUUID);
127 |
128 | BaseRequest loginInfo = weChatLogin.getLoginInfo();
129 |
130 | this.logger = LogManager.getLogger(loginInfo.getUin());
131 |
132 | this.initWeChatCore(loginInfo);
133 |
134 | this.start();
135 | }
136 |
137 | public void stop(){
138 | }
139 |
140 |
141 |
142 | public File getDataFolder(){
143 | return new File(System.getProperty("user.dir"));
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/WeChatCoreImpl.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl;
2 |
3 | import com.meteor.wechatbc.WeChatCore;
4 | import com.meteor.wechatbc.entitiy.session.BaseRequest;
5 | import com.meteor.wechatbc.impl.model.Session;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import org.apache.logging.log4j.Logger;
9 |
10 | public class WeChatCoreImpl implements WeChatCore {
11 |
12 | private WeChatClient weChatClient;
13 | @Getter @Setter private Session session;
14 |
15 | private HttpAPI httpAPI;
16 |
17 |
18 | public WeChatCoreImpl(WeChatClient weChatClient,Session session){
19 | this.weChatClient = weChatClient;
20 | this.session = session;
21 | this.httpAPI = new HttpAPIImpl(this.weChatClient);
22 | }
23 |
24 | public WeChatCoreImpl(WeChatClient weChatClient, BaseRequest baseRequest){
25 | this.weChatClient = weChatClient;
26 | this.session = new Session();
27 | this.session.setBaseRequest(baseRequest);
28 | this.httpAPI = new HttpAPIImpl(this.weChatClient);
29 | }
30 |
31 | @Override
32 | public HttpAPI getHttpAPI() {
33 | return httpAPI;
34 | }
35 |
36 | @Override
37 | public String getAPIVersion() {
38 | return null;
39 | }
40 |
41 | @Override
42 | public Logger getLogger() {
43 | return null;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/command/CommandManager.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.command;
2 |
3 | import com.meteor.wechatbc.command.WeChatCommand;
4 | import com.meteor.wechatbc.entitiy.message.Message;
5 | import com.meteor.wechatbc.impl.model.MsgType;
6 | import lombok.Data;
7 | import lombok.Getter;
8 | import org.apache.logging.log4j.LogManager;
9 | import org.apache.logging.log4j.Logger;
10 |
11 | import java.util.Arrays;
12 | import java.util.Map;
13 | import java.util.concurrent.ConcurrentHashMap;
14 |
15 | /**
16 | * 指令管理
17 | */
18 | public class CommandManager {
19 |
20 | // 存储所有注册的指令
21 | private Map weChatCommandMap = new ConcurrentHashMap<>();
22 |
23 | @Getter private final Logger logger = LogManager.getLogger("CommandManager");
24 |
25 | public Map getWeChatCommandMap() {
26 | return weChatCommandMap;
27 | }
28 |
29 | public void registerCommand(WeChatCommand weChatCommand){
30 | String mainCommand = weChatCommand.getMainCommand();
31 |
32 | if(weChatCommandMap.containsKey(mainCommand)){
33 | logger.warn("注册指令" + weChatCommand.getMainCommand()+"时发生了冲突");
34 | return;
35 | }
36 | logger.debug("注册了指令 {}",mainCommand);
37 | weChatCommandMap.put(mainCommand,weChatCommand);
38 | }
39 |
40 | @Data
41 | public class ExecutorCommand{
42 |
43 | private String command;
44 |
45 | public ExecutorCommand(String command){
46 | this.command = command;
47 | }
48 |
49 | // 取得指令执行的子参数
50 | // 例如对于 test ag1 ag2 得到[ag1,ag2]元素
51 | public String[] formatArgs(){
52 | return Arrays.stream(command.split(" "))
53 | .skip(1)
54 | .toArray(String[]::new);
55 | }
56 |
57 | public String getMainCommand(){
58 | if(!command.contains(" ")) return command;
59 | return command.substring(0,command.indexOf(" "));
60 | }
61 | }
62 |
63 | public ExecutorCommand getExecutorCommand(String command){
64 | return new ExecutorCommand(command);
65 | }
66 |
67 |
68 | public String[] formatArgs(String command){
69 | return new ExecutorCommand(command).formatArgs();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/console/Console.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.console;
2 |
3 | import com.alibaba.fastjson2.JSONObject;
4 | import com.meteor.wechatbc.command.WeChatCommand;
5 | import com.meteor.wechatbc.command.sender.ConsoleSender;
6 | import com.meteor.wechatbc.entitiy.contact.Contact;
7 | import com.meteor.wechatbc.entitiy.contact.GetBatchContact;
8 | import com.meteor.wechatbc.entitiy.message.SentMessage;
9 | import com.meteor.wechatbc.impl.WeChatClient;
10 | import com.meteor.wechatbc.impl.command.CommandManager;
11 | import net.minecrell.terminalconsole.SimpleTerminalConsole;
12 | import org.jline.reader.LineReader;
13 | import org.jline.reader.LineReaderBuilder;
14 | import org.jline.reader.impl.history.DefaultHistory;
15 | import java.util.Optional;
16 |
17 | /**
18 | * 控制台
19 | */
20 | public class Console extends SimpleTerminalConsole {
21 |
22 | private final WeChatClient weChatClient;
23 |
24 | public Console(WeChatClient weChatClient){
25 | this.weChatClient = weChatClient;
26 | this.consoleSender = new ConsoleSender(weChatClient);
27 | }
28 |
29 | private final ConsoleSender consoleSender; // 控制台指令执行者
30 |
31 | @Override
32 | protected boolean isRunning() {
33 | return true;
34 | }
35 |
36 | @Override
37 | protected void runCommand(String command) {
38 | CommandManager commandManager = weChatClient.getCommandManager();
39 | CommandManager.ExecutorCommand executorCommand = commandManager.getExecutorCommand(command);
40 | Optional.ofNullable(commandManager.getWeChatCommandMap().get(executorCommand.getMainCommand())).ifPresent(weChatCommand -> {
41 | weChatCommand.getCommandExecutor().onCommand(consoleSender, executorCommand.formatArgs());
42 | });
43 | }
44 |
45 | @Override
46 | protected void shutdown() {
47 | }
48 |
49 | @Override
50 | protected LineReader buildReader(LineReaderBuilder builder) {
51 | return builder.history(new DefaultHistory()).build();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/contact/ContactManager.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.contact;
2 |
3 |
4 | import com.alibaba.fastjson2.JSON;
5 | import com.alibaba.fastjson2.JSONArray;
6 | import com.alibaba.fastjson2.JSONObject;
7 | import com.github.benmanes.caffeine.cache.CacheLoader;
8 | import com.github.benmanes.caffeine.cache.Caffeine;
9 | import com.github.benmanes.caffeine.cache.LoadingCache;
10 | import com.meteor.wechatbc.impl.HttpAPI;
11 | import com.meteor.wechatbc.entitiy.contact.Contact;
12 | import com.meteor.wechatbc.impl.WeChatClient;
13 | import lombok.Getter;
14 | import org.checkerframework.checker.nullness.qual.NonNull;
15 | import org.checkerframework.checker.nullness.qual.Nullable;
16 |
17 | import java.util.HashMap;
18 | import java.util.Map;
19 | import java.util.concurrent.TimeUnit;
20 |
21 | /**
22 | * 微信联系人
23 | */
24 | public class ContactManager {
25 |
26 |
27 | private class ContactCacheLoader implements CacheLoader{
28 | @Override
29 | public @Nullable Contact load(@NonNull String key) throws Exception {
30 | return getContact(key);
31 | }
32 | }
33 |
34 | private final WeChatClient weChatClient;
35 |
36 | @Getter private LoadingCache contactCache
37 | = Caffeine.newBuilder().maximumSize(1000)
38 | .expireAfterWrite(30, TimeUnit.MINUTES)
39 | .build(new ContactCacheLoader());
40 |
41 |
42 | private Map retrievalTypeRetrievalStrategyMap;
43 |
44 | @Getter private Map contactMap;
45 |
46 | public ContactManager(WeChatClient weChatClient){
47 | this.weChatClient = weChatClient;
48 | this.contactMap = ref();
49 |
50 | this.retrievalTypeRetrievalStrategyMap = new HashMap<>();
51 |
52 | retrievalTypeRetrievalStrategyMap.put(RetrievalType.NICK_NAME,new NickNameStrategy(this.contactMap));
53 | retrievalTypeRetrievalStrategyMap.put(RetrievalType.USER_NAME,new UserNameStrategy());
54 | retrievalTypeRetrievalStrategyMap.put(RetrievalType.REMARK_NAME,new RemarkStrategy(this.contactMap));
55 |
56 | this.weChatClient.getLogger().info("联系人列表数量: "+contactMap.size());
57 | }
58 |
59 | /**
60 | * 根据搜索策略寻找用户
61 | * @param retrievalType
62 | * @param targetKey
63 | * @return
64 | */
65 | public Contact getContact(RetrievalType retrievalType,String targetKey){
66 | return retrievalTypeRetrievalStrategyMap.get(retrievalType).getContact(contactMap,targetKey);
67 | }
68 |
69 | public Contact getContactByNickName(String nickName){
70 | return retrievalTypeRetrievalStrategyMap.get(RetrievalType.NICK_NAME).getContact(contactMap,nickName);
71 | }
72 |
73 | public Contact getContactByRemark(String remark){
74 | return retrievalTypeRetrievalStrategyMap.get(RetrievalType.REMARK_NAME).getContact(contactMap,remark);
75 | }
76 |
77 | public Contact getContact(String userName){
78 |
79 | Contact user = weChatClient.getWeChatCore().getSession().getWxInitInfo().getUser();
80 | if(user.getUserName().equalsIgnoreCase(userName)){
81 | return user;
82 | }
83 |
84 | if(contactMap.containsKey(userName)) return contactMap.get(userName);
85 |
86 | else {
87 | this.contactMap = ref();
88 | return contactMap.get(userName);
89 | }
90 | }
91 |
92 | /**
93 | * 刷新联系人列表
94 | */
95 | public Map ref(){
96 | HttpAPI httpAPI = weChatClient.getWeChatCore().getHttpAPI();
97 | JSONObject response = httpAPI.getContact();
98 | JSONArray memberList = response.getJSONArray("MemberList");
99 | Map map = new HashMap<>();
100 | for (int i = 0; i < memberList.size(); i++) {
101 | JSONObject jsonObject = memberList.getJSONObject(i);
102 | Contact contact = JSON.toJavaObject(jsonObject, Contact.class);
103 | contact.setWeChatClient(this.weChatClient);
104 | map.put(contact.getUserName(),contact);
105 | }
106 | return map;
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/contact/NickNameStrategy.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.contact;
2 |
3 | import com.meteor.wechatbc.entitiy.contact.Contact;
4 |
5 | import java.util.Map;
6 | import java.util.concurrent.ConcurrentHashMap;
7 |
8 | /**
9 | * 根据名称获取
10 | */
11 | public class NickNameStrategy implements RetrievalStrategy{
12 |
13 | private Map contactMap;
14 |
15 | public NickNameStrategy(Map source){
16 | contactMap = new ConcurrentHashMap<>();
17 | source.values().stream().forEach(contact -> {
18 | contactMap.put(contact.getNickName(),contact);
19 | });
20 | }
21 |
22 |
23 | @Override
24 | public Contact getContact(Map source, String key) {
25 |
26 | if(contactMap.containsKey(key)) return contactMap.get(key);
27 | else {
28 | for (Contact value : source.values()) {
29 | if(value.getNickName().equalsIgnoreCase(key)) {
30 | contactMap.put(key,value);
31 | return value;
32 | }
33 | }
34 | }
35 |
36 | return null;
37 |
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/contact/RemarkStrategy.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.contact;
2 |
3 | import com.meteor.wechatbc.entitiy.contact.Contact;
4 |
5 | import java.util.Map;
6 | import java.util.concurrent.ConcurrentHashMap;
7 |
8 | /**
9 | * 根据备注获取
10 | */
11 | public class RemarkStrategy implements RetrievalStrategy{
12 |
13 | private Map contactMap;
14 |
15 | public RemarkStrategy(Map source){
16 | contactMap = new ConcurrentHashMap<>();
17 | source.values().stream().forEach(contact -> {
18 | contactMap.put(contact.getRemarkName(),contact);
19 | });
20 | }
21 |
22 |
23 | @Override
24 | public Contact getContact(Map source, String key) {
25 |
26 | if(contactMap.containsKey(key)) return contactMap.get(key);
27 | else {
28 | for (Contact value : source.values()) {
29 | if(value.getRemarkName().equalsIgnoreCase(key)) {
30 | contactMap.put(key,value);
31 | return value;
32 | }
33 | }
34 | }
35 |
36 | return null;
37 |
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/contact/RetrievalStrategy.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.contact;
2 |
3 | import com.meteor.wechatbc.entitiy.contact.Contact;
4 |
5 | import java.util.Map;
6 |
7 | /**
8 | * 搜索策略
9 | */
10 | public interface RetrievalStrategy {
11 | Contact getContact(Map source, String key);
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/contact/RetrievalType.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.contact;
2 |
3 | public enum RetrievalType {
4 |
5 | NICK_NAME("根据昵称查询"),USER_NAME("根据username查询"),REMARK_NAME("根据备注查询");
6 | private String comment;
7 |
8 | RetrievalType(String comment){
9 | this.comment = comment;
10 | }
11 |
12 | public String getComment() {
13 | return comment;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/contact/UserNameStrategy.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.contact;
2 |
3 | import com.meteor.wechatbc.entitiy.contact.Contact;
4 |
5 | import java.util.Map;
6 |
7 | public class UserNameStrategy implements RetrievalStrategy{
8 | @Override
9 | public Contact getContact(Map source, String key) {
10 | return source.get(key);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/cookie/CookiePack.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.cookie;
2 |
3 | import lombok.Getter;
4 | import okhttp3.Cookie;
5 |
6 | import java.io.*;
7 |
8 | /**
9 | * 代理cookie类以实现序列化
10 | */
11 | public class CookiePack implements Serializable {
12 |
13 | @Getter private Cookie cookie;
14 |
15 | public CookiePack(Cookie cookie){
16 | this.cookie = cookie;
17 | }
18 |
19 | /**
20 | * 以16进制字符串的形式编码
21 | * @return
22 | */
23 | public String encode(){
24 | try(
25 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
26 | ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
27 | ) {
28 | objectOutputStream.writeObject(this);
29 | return toHexoString(byteArrayOutputStream.toByteArray());
30 | } catch (IOException e) {
31 | throw new RuntimeException(e);
32 | }
33 | }
34 |
35 | private String toHexoString(byte[] bytes){
36 | StringBuilder sb = new StringBuilder();
37 | for (byte b : bytes) {
38 | sb.append(String.format("%02X", b));
39 | }
40 | return sb.toString();
41 | }
42 |
43 | /**
44 | * 将16进制字符串转换为字节数组
45 | * @param hexString
46 | * @return
47 | */
48 | public static byte[] hexStringToBytes(String hexString) {
49 | int len = hexString.length();
50 | byte[] data = new byte[len / 2];
51 | for (int i = 0; i < len; i += 2) {
52 | data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
53 | + Character.digit(hexString.charAt(i+1), 16));
54 | }
55 | return data;
56 | }
57 |
58 | /**
59 | * 以16进制字符串的形式解码
60 | */
61 | public static CookiePack decode(String encodeCookie){
62 | byte[] bytes = hexStringToBytes(encodeCookie);
63 | try(
64 | ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(bytes);
65 | ObjectInputStream objectInputStream = new ObjectInputStream(arrayInputStream);
66 | ) {
67 | return CookiePack.class.cast(objectInputStream.readObject());
68 | } catch (IOException e) {
69 | throw new RuntimeException(e);
70 | } catch (ClassNotFoundException e) {
71 | throw new RuntimeException(e);
72 | }
73 | }
74 |
75 | private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
76 | objectOutputStream.writeObject(cookie.name());
77 | objectOutputStream.writeObject(cookie.value());
78 | objectOutputStream.writeLong(cookie.persistent()?cookie.expiresAt():-1);
79 | objectOutputStream.writeObject(cookie.domain());
80 | objectOutputStream.writeObject(cookie.path());
81 | objectOutputStream.writeBoolean(cookie.secure());
82 | objectOutputStream.writeBoolean(cookie.httpOnly());
83 | objectOutputStream.writeBoolean(cookie.hostOnly());
84 | }
85 |
86 | private void reloadObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
87 | Cookie.Builder builder = new Cookie.Builder();
88 | builder.name((String)objectInputStream.readObject());
89 | builder.value((String)objectInputStream.readObject());
90 |
91 | long l = objectInputStream.readLong();
92 |
93 | if(l!=-1) builder.expiresAt(l);
94 | String domain = null;
95 | builder.domain(domain = (String)objectInputStream.readObject());
96 | builder.path((String)objectInputStream.readObject());
97 |
98 | boolean secure = objectInputStream.readBoolean();
99 | if(secure) builder.secure();
100 | boolean httpOnly = objectInputStream.readBoolean();
101 | if(httpOnly) builder.httpOnly();
102 | boolean hostOnly = objectInputStream.readBoolean();
103 | if(hostOnly) builder.hostOnlyDomain(domain);
104 |
105 | this.cookie = builder.build();
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/cookie/WeChatCookie.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.cookie;
2 |
3 | import okhttp3.Cookie;
4 | import okhttp3.CookieJar;
5 | import okhttp3.HttpUrl;
6 |
7 | import java.util.HashMap;
8 | import java.util.List;
9 | import java.util.Map;
10 |
11 | public class WeChatCookie implements CookieJar {
12 |
13 | // 登陆后初始化的Cookie
14 | private List initCookie;
15 | private Map> cookieListMap;
16 |
17 | public WeChatCookie(List initCookie){
18 | this.initCookie = initCookie;
19 | this.cookieListMap = new HashMap<>();
20 | }
21 |
22 | @Override
23 | public void saveFromResponse(HttpUrl httpUrl, List list) {
24 | cookieListMap.put(httpUrl,list);
25 | }
26 |
27 |
28 | @Override
29 | public List loadForRequest(HttpUrl httpUrl) {
30 | return cookieListMap.getOrDefault(httpUrl,initCookie);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/EventManager.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event;
2 |
3 | import com.meteor.wechatbc.event.Event;
4 | import com.meteor.wechatbc.event.EventBus;
5 | import com.meteor.wechatbc.impl.DefaultPlugin;
6 | import com.meteor.wechatbc.impl.WeChatClient;
7 | import com.meteor.wechatbc.impl.event.listener.ContactCommandListener;
8 | import com.meteor.wechatbc.plugin.Plugin;
9 | import lombok.Getter;
10 |
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.Map;
14 | import java.util.concurrent.ConcurrentHashMap;
15 |
16 | /**
17 | * 事件管理器
18 | */
19 | public class EventManager {
20 |
21 | @Getter private WeChatClient weChatClient;
22 |
23 | private final EventBus eventBus;
24 |
25 | @Getter private final DefaultPlugin defaultPlugin = new DefaultPlugin();
26 |
27 | private final Map> pluginListeners = new ConcurrentHashMap<>();
28 |
29 | public EventManager(WeChatClient weChatClient){
30 | this.eventBus = new EventBus();
31 | this.weChatClient = weChatClient;
32 | this.registerDefaultListener();
33 | }
34 |
35 | /**
36 | * 载入一些预定义的监听器
37 | */
38 | public void registerDefaultListener(){
39 | registerPluginListener(defaultPlugin,new ContactCommandListener(weChatClient));
40 | }
41 |
42 | /**
43 | * 注册插件监听器
44 | * @param plugin
45 | * @param listener
46 | */
47 | public void registerPluginListener(Plugin plugin,Listener listener){
48 | eventBus.register(listener);
49 | pluginListeners.putIfAbsent(plugin,new ArrayList<>());
50 | pluginListeners.get(plugin).add(listener);
51 | }
52 |
53 | /**
54 | * 取消插件所有监听器
55 | * @param plugin
56 | */
57 | public void unRegisterPluginListener(Plugin plugin){
58 | List listeners = pluginListeners.get(plugin);
59 | if(listeners!=null) {
60 | listeners.forEach(this::unRegisterListener);
61 | }
62 | pluginListeners.remove(plugin);
63 | }
64 |
65 | private void unRegisterListener(Listener listener){
66 | eventBus.unRegister(listener);
67 | }
68 |
69 | /**
70 | * 呼叫事件
71 | */
72 | public void callEvent(Event event){
73 | event.setEventManager(this);
74 | eventBus.post(event);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/Listener.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event;
2 |
3 | /**
4 | * 所有监听器类必须实现此接口
5 | */
6 | public interface Listener {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/listener/ContactCommandListener.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event.listener;
2 |
3 | import com.meteor.wechatbc.command.sender.ContactSender;
4 | import com.meteor.wechatbc.entitiy.contact.Contact;
5 | import com.meteor.wechatbc.entitiy.message.Message;
6 | import com.meteor.wechatbc.event.EventHandler;
7 | import com.meteor.wechatbc.impl.WeChatClient;
8 | import com.meteor.wechatbc.impl.command.CommandManager;
9 | import com.meteor.wechatbc.impl.event.Listener;
10 | import com.meteor.wechatbc.impl.event.sub.MessageEvent;
11 | import com.meteor.wechatbc.impl.event.sub.ReceiveMessageEvent;
12 | import lombok.AllArgsConstructor;
13 |
14 | import java.util.Optional;
15 |
16 | /**
17 | * 监听微信用户执行指令
18 | */
19 | @AllArgsConstructor
20 | public class ContactCommandListener implements Listener {
21 |
22 | private WeChatClient weChatClient;
23 |
24 | @EventHandler
25 | public void onReceiveMessage(MessageEvent messageEvent){
26 | Message message = messageEvent.getMessage();
27 |
28 | }
29 |
30 |
31 | @EventHandler
32 | public void onCommand(MessageEvent messageEvent){
33 | String content = messageEvent.getContent();
34 | Message message = messageEvent.getMessage();
35 | String fromUserName = message.getFromUserName();
36 | Contact contact = weChatClient.getContactManager().getContactCache().get(fromUserName);
37 | if(contact==null) return;
38 | if(content.startsWith("/")){
39 | CommandManager commandManager = weChatClient.getCommandManager();
40 | CommandManager.ExecutorCommand executorCommand = commandManager.getExecutorCommand(content.replace("/",""));
41 | Optional.ofNullable(commandManager.getWeChatCommandMap().get(executorCommand.getMainCommand())).ifPresent(weChatCommand -> {
42 | weChatCommand.getCommandExecutor().onCommand(new ContactSender(weChatClient.getContactManager().getContactCache().get(message.getSenderUserName())
43 | ,fromUserName
44 | ),executorCommand.formatArgs());
45 | });
46 | }
47 |
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/sub/ClientDeathEvent.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event.sub;
2 |
3 | import com.meteor.wechatbc.entitiy.synccheck.SyncCheckSelector;
4 | import com.meteor.wechatbc.event.Event;
5 | import com.meteor.wechatbc.impl.WeChatClient;
6 | import lombok.AllArgsConstructor;
7 | import lombok.Data;
8 |
9 | /**
10 | * 微信登出事件
11 | */
12 | @AllArgsConstructor
13 | @Data
14 | public class ClientDeathEvent extends Event {
15 |
16 | private WeChatClient weChatClient;
17 |
18 | private SyncCheckSelector syncCheckSelector;
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/sub/GroupMessageEvent.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event.sub;
2 |
3 | import com.meteor.wechatbc.entitiy.contact.Contact;
4 | import com.meteor.wechatbc.entitiy.message.Message;
5 |
6 | /**
7 | * 群聊消息
8 | */
9 | public class GroupMessageEvent extends ReceiveMessageEvent{
10 |
11 | public GroupMessageEvent(Message message) {
12 | super(message);
13 | }
14 |
15 | /**
16 | * 获取发送消息的群聊
17 | * @return
18 | */
19 | public Contact getGroup(){
20 | Message message = getMessage();
21 | Contact contact = getEventManager().getWeChatClient().getContactManager().getContactCache().get(message.getFromUserName());
22 | return contact;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/sub/MessageEvent.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event.sub;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 | import com.meteor.wechatbc.event.Event;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 |
8 | /**
9 | * 所有消息事件的基类
10 | */
11 | @AllArgsConstructor
12 | public class MessageEvent extends Event {
13 | @Getter private Message message;
14 |
15 | public String getContent(){
16 | return message.getContent();
17 | }
18 |
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/sub/OwnerMessageEvent.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event.sub;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 |
5 | public class OwnerMessageEvent extends MessageEvent{
6 | public OwnerMessageEvent(Message message) {
7 | super(message);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/event/sub/ReceiveMessageEvent.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.event.sub;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 |
5 | public class ReceiveMessageEvent extends MessageEvent{
6 | public ReceiveMessageEvent(Message message) {
7 | super(message);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/fileupload/FileChunkUploader.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.fileupload;
2 |
3 | import com.alibaba.fastjson2.JSON;
4 | import com.alibaba.fastjson2.JSONObject;
5 | import com.meteor.wechatbc.impl.HttpAPIImpl;
6 | import com.meteor.wechatbc.impl.WeChatClient;
7 | import com.meteor.wechatbc.impl.fileupload.model.UploadMediaRequest;
8 | import com.meteor.wechatbc.impl.fileupload.model.UploadResponse;
9 | import com.meteor.wechatbc.impl.model.Session;
10 | import com.meteor.wechatbc.util.URL;
11 | import okhttp3.*;
12 | import okio.BufferedSink;
13 | import org.apache.logging.log4j.LogManager;
14 | import org.apache.logging.log4j.Logger;
15 |
16 | import javax.activation.MimetypesFileTypeMap;
17 | import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
18 | import java.io.*;
19 | import java.net.FileNameMap;
20 | import java.net.URLConnection;
21 | import java.nio.file.Files;
22 | import java.security.MessageDigest;
23 | import java.security.NoSuchAlgorithmException;
24 | import java.util.Arrays;
25 | import java.util.Date;
26 | import java.util.HashSet;
27 | import java.util.Set;
28 |
29 |
30 | /**
31 | * 分块上传文件至微信服务器
32 | */
33 | public class FileChunkUploader {
34 |
35 | private final WeChatClient weChatClient;
36 |
37 | private final HttpAPIImpl httpAPI;
38 |
39 | private final Logger logger = LogManager.getLogger(FileChunkUploader.class);
40 |
41 | private FileChunkUploader(WeChatClient weChatClient){
42 | this.weChatClient = weChatClient;
43 | this.httpAPI = ((HttpAPIImpl)weChatClient.getWeChatCore().getHttpAPI());
44 | }
45 |
46 | public static FileChunkUploader INSTANCE;
47 |
48 | public static void init(WeChatClient weChatClient){
49 | INSTANCE = new FileChunkUploader(weChatClient);
50 | }
51 |
52 | private final long CHUNK_SIZE = 524288; // 分块大小 : 5m
53 |
54 | /**
55 | * 文件类型
56 | */
57 | private static class FileTypeDetector {
58 | private static final String PIC = "pic";
59 | private static final String VIDEO = "video";
60 | private static final String DOC = "doc";
61 |
62 | // 支持的图片格式
63 | private static final Set imageType = new HashSet<>(Arrays.asList("jpg", "jpeg", "png","gif"));
64 | // 视频文件扩展名
65 | private static final String videoType = "mp4";
66 |
67 | // 获取文件类型
68 | public static String getMessageType(String filename) {
69 | String ext = getFileExt(filename).toLowerCase();
70 | if (imageType.contains(ext)) {
71 | return PIC;
72 | }
73 | if (ext.equals(videoType)) {
74 | return VIDEO;
75 | }
76 | return DOC;
77 | }
78 |
79 | private static String getFileExt(String filename) {
80 | int dotIndex = filename.lastIndexOf('.');
81 | if (dotIndex > 0 && dotIndex < filename.length() - 1) {
82 | return filename.substring(dotIndex + 1);
83 | }
84 | return "";
85 | }
86 | }
87 |
88 |
89 | public UploadResponse upload(File file, UploadMediaRequest uploadMediaRequest){
90 |
91 | Session session = weChatClient.getWeChatCore().getSession();
92 |
93 |
94 | try {
95 |
96 | String filename = file.getName();
97 |
98 | // 获取文件上传类型
99 | MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap();
100 | String mimeType = fileTypeMap.getContentType(file);
101 |
102 | // 计算文件md5
103 | String md5 = calculateFileMD5(file);
104 | // 文件类型
105 | String messageType = FileTypeDetector.getMessageType(file.getName());
106 |
107 | long fileSize = file.length();
108 | // 分块数量 (向上取整)
109 | long chunks = (fileSize + CHUNK_SIZE - 1) / CHUNK_SIZE;
110 |
111 | uploadMediaRequest.setDataLen(fileSize);
112 | uploadMediaRequest.setTotalLen(fileSize);
113 | uploadMediaRequest.setFiledMD5(md5);
114 | uploadMediaRequest.setClientMediaId(System.currentTimeMillis() / 1000);
115 | uploadMediaRequest.setBaseRequest(session.getBaseRequest());
116 | uploadMediaRequest.setFromUserName(session.getWxInitInfo().getUser().getUserName());
117 |
118 | for(int chunk=0;chunk 0) {
135 | int read = raf.read(buffer, 0, (int)Math.min(buffer.length, remaining));
136 | if (read == -1) break;
137 | sink.write(buffer, 0, read);
138 | remaining -= read;
139 | }
140 | }
141 | }
142 | @Override
143 | public long contentLength() throws IOException {
144 | return chunkLength; // 当前块的大小
145 | }
146 | };
147 | MultipartBody.Builder builder = new MultipartBody.Builder()
148 | .setType(MultipartBody.FORM)
149 | .addFormDataPart("id", "WECHAT_BC")
150 | .addFormDataPart("name", filename)
151 | .addFormDataPart("type", mimeType)
152 | .addFormDataPart("size", String.valueOf(fileSize))
153 | .addFormDataPart("mediatype", messageType)
154 | .addFormDataPart("uploadmediarequest", JSON.toJSONString(uploadMediaRequest)) // 构建上传请求JSON
155 | .addFormDataPart("webwx_data_ticket", session.getBaseRequest().getDataTicket())
156 | .addFormDataPart("pass_ticket", session.getBaseRequest().getPassTicket())
157 | .addFormDataPart("filename", filename)
158 | .addFormDataPart("lastModifiedDate", new Date().toString())
159 | .addFormDataPart("chunk", String.valueOf(chunk))
160 | .addFormDataPart("filename",filename,chunkBody);
161 | if(chunks>1){
162 | builder.addFormDataPart("chunks", String.valueOf(chunks));
163 | }
164 | HttpUrl httpUrl = URL.BASE_URL.newBuilder()
165 | .encodedPath(URL.UPLOAD_FILE)
166 | .addQueryParameter("f","json")
167 | .build();
168 | Request request = httpAPI.getBASE_REQUEST()
169 | .newBuilder()
170 | .url(httpUrl)
171 | .post(builder.build())
172 | .build();
173 | try(
174 | Response response = httpAPI.getOkHttpClient().newCall(request).execute();
175 | ) {
176 | logger.debug("[{}] {} / {}",filename,chunk+1,chunks);
177 | UploadResponse uploadResponse = JSON.toJavaObject(response.body().string(), UploadResponse.class);
178 | // 检查中途块传输是否出现问题
179 | if(chunk != chunks-1){
180 | if(!uploadResponse.isFull()){
181 | logger.error("{} 传输出现错误",filename);
182 | throw new RuntimeException("文件上传出现错误,文件名: "+filename);
183 | }
184 | }else {
185 | logger.info(uploadResponse.toString());
186 | return uploadResponse;
187 | }
188 | }
189 | }
190 | } catch (IOException e) {
191 | throw new RuntimeException(e);
192 | } catch (NoSuchAlgorithmException e) {
193 | throw new RuntimeException(e);
194 | }
195 | return null;
196 | }
197 |
198 |
199 | /**
200 | * 计算文件md5值
201 | * @param file
202 | * @return
203 | * @throws IOException
204 | * @throws NoSuchAlgorithmException
205 | */
206 | private String calculateFileMD5(File file) throws IOException, NoSuchAlgorithmException {
207 | MessageDigest digest = MessageDigest.getInstance("MD5");
208 | try (InputStream is = new FileInputStream(file)) {
209 | byte[] buffer = new byte[8192];
210 | int read;
211 | while ((read = is.read(buffer)) != -1) {
212 | digest.update(buffer, 0, read);
213 | }
214 | }
215 | return new HexBinaryAdapter().marshal(digest.digest()).toLowerCase();
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/fileupload/model/BaseResponse.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.fileupload.model;
2 |
3 | import lombok.Data;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 |
7 | @Data
8 | public class BaseResponse {
9 | private int Ret;
10 | private String ErrMsg;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/fileupload/model/UploadMediaRequest.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.fileupload.model;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import com.meteor.wechatbc.entitiy.session.BaseRequest;
5 | import lombok.Builder;
6 | import lombok.Data;
7 |
8 | @Data
9 | @Builder
10 | public class UploadMediaRequest {
11 | @JSONField(name = "UploadType")
12 | private int uploadType = 2;
13 | @JSONField(name = "BaseRequest")
14 | private BaseRequest baseRequest;
15 | @JSONField(name = "ClientMediaId")
16 | private long clientMediaId;
17 | @JSONField(name = "TotalLen")
18 | private long totalLen;
19 | @JSONField(name = "StartPos")
20 | private long startPos = 0;
21 | @JSONField(name = "DataLen")
22 | private long dataLen;
23 | @JSONField(name = "MediaType")
24 | private long mediaType = 4;
25 | @JSONField(name = "FromUserName")
26 | private String fromUserName;
27 | @JSONField(name = "ToUserName")
28 | private String toUserName;
29 | @JSONField(name = "FileMd5")
30 | private String filedMD5;
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/fileupload/model/UploadResponse.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.fileupload.model;
2 |
3 | import com.alibaba.fastjson.annotation.JSONField;
4 | import lombok.Data;
5 | import lombok.ToString;
6 |
7 | @Data
8 | @ToString
9 | public class UploadResponse {
10 | @JSONField(name = "BaseResponse")
11 | private BaseResponse baseResponse;
12 |
13 | @JSONField(name = "MediaId")
14 | private String mediaId;
15 |
16 | @JSONField(name = "StartPos")
17 | private long startPos;
18 |
19 | @JSONField(name = "CDNThumbImgHeight")
20 | private int cdnThumbImgHeight;
21 |
22 | @JSONField(name = "CDNThumbImgWidth")
23 | private int cdnThumbImgWidth;
24 |
25 | @JSONField(name = "EncryFileName")
26 | private String encryFileName;
27 |
28 | /**
29 | * 是否完整
30 | */
31 | public boolean isFull(){
32 | return getBaseResponse().getRet() == 0;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/interceptor/WeChatInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.interceptor;
2 |
3 | import com.alibaba.fastjson2.JSON;
4 | import com.alibaba.fastjson2.JSONObject;
5 | import com.meteor.wechatbc.impl.WeChatCoreImpl;
6 | import com.meteor.wechatbc.impl.model.Session;
7 | import com.meteor.wechatbc.util.URL;
8 | import okhttp3.*;
9 | import okio.Buffer;
10 |
11 | import java.io.IOException;
12 | import java.util.Arrays;
13 | import java.util.Date;
14 | import java.util.List;
15 |
16 | /**
17 | * 拦截器
18 | * 进行BaseRequest公参的添加
19 | */
20 | public class WeChatInterceptor implements Interceptor {
21 |
22 | private WeChatCoreImpl weChatCore;
23 |
24 | public WeChatInterceptor(WeChatCoreImpl weChatCore){
25 | this.weChatCore = weChatCore;
26 | }
27 |
28 | private String bodyToString(final RequestBody body) {
29 | try (Buffer buffer = new Buffer()) {
30 | if (body != null) {
31 | body.writeTo(buffer);
32 | } else {
33 | return "";
34 | }
35 | return buffer.readUtf8();
36 | } catch (IOException e) {
37 | return "Did not work";
38 | }
39 | }
40 |
41 | // 不进行拦截操作的url名单
42 | private static List BLACK_URL
43 | = Arrays.asList(URL.UPLOAD_FILE);
44 |
45 | @Override
46 | public Response intercept(Chain chain) throws IOException {
47 | Request originalRequest = chain.request();
48 |
49 | // 如果在黑名单里不进行任何操作
50 | if(BLACK_URL.contains(originalRequest.url().encodedPath())) return chain.proceed(chain.request());
51 |
52 |
53 | // 仅拦截post请求
54 | if(!originalRequest.method().equalsIgnoreCase("POST")) return chain.proceed(chain.request());
55 |
56 | Session session = weChatCore.getSession();
57 | RequestBody originalBody = originalRequest.body();
58 |
59 | MediaType mediaType = originalBody.contentType();
60 |
61 |
62 | // 请求体转换为JSON对象
63 | JSONObject originalJson = JSON.parseObject(bodyToString(originalBody), JSONObject.class);
64 |
65 | // 添加公共BaseRequest
66 | originalJson.put("BaseRequest", session.getBaseRequest());
67 |
68 | Request request = chain.request();
69 |
70 | if(request.url().encodedPath().equalsIgnoreCase(URL.WEBWXSYNC)){
71 | originalJson.put("rr",String.valueOf(-new Date().getTime() / 1000));
72 | }
73 |
74 | RequestBody requestBody = RequestBody.create(mediaType, JSON.toJSONString(originalJson));
75 |
76 | // 构建新的请求
77 | Request newRequest = originalRequest.newBuilder()
78 | .method(originalRequest.method(), requestBody)
79 | .build();
80 |
81 | return chain.proceed(newRequest);
82 | }
83 |
84 |
85 |
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/MsgType.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 | import com.meteor.wechatbc.impl.model.message.*;
5 |
6 | public enum MsgType{
7 | TextMsg("1"), //文本消息
8 | ImgEmoteMsg("47"), // 图片表情
9 | ImageMsg("3"),// 图片消息
10 | VideoMsg("43"), // 视频消息
11 | APPMsg("49"), // APP消息
12 | SysMsg("10000"), // 系统消息
13 | VoiceMsg("34"), // 语音消息
14 |
15 | RevokeMsg("10002"); // 撤回消息
16 |
17 | /**
18 | * 未处理的消息
19 | PossibleFriendMsg(40), // 好友推荐消息
20 | ContactCardMsg(42), // 名片消息
21 | RecalledMsg(10002); // 撤回消息
22 | **/
23 |
24 | private String idx;
25 |
26 |
27 | MsgType(String idx) {
28 | this.idx = idx;
29 | }
30 |
31 | public String getIdx() {
32 | return idx;
33 | }
34 |
35 | public static MsgType fromIdx(String idx){
36 | for (MsgType value : MsgType.values()) {
37 | if(value.getIdx().equalsIgnoreCase(idx)) return value;
38 | }
39 | return TextMsg;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/Session.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 | import com.meteor.wechatbc.entitiy.session.BaseRequest;
5 | import com.meteor.wechatbc.entitiy.session.SyncKey;
6 | import lombok.Data;
7 | import lombok.ToString;
8 |
9 | import java.io.*;
10 |
11 | /**
12 | * 会话缓存,用于维持与微信链接
13 | */
14 | @Data
15 | @ToString
16 | public class Session implements Serializable {
17 | @JSONField(name = "BaseRequest")
18 | BaseRequest baseRequest;
19 |
20 | @JSONField(serialize = false)
21 | WxInitInfo wxInitInfo;
22 |
23 | // 作为同步消息的synckey,不参与序列化
24 | SyncKey checkSyncKey;
25 |
26 | // 获取消息的SyncKey
27 | @JSONField(name = "SyncKey")
28 | SyncKey syncKey;
29 |
30 | public void setCheckSyncKey(SyncKey checkSyncKey) {
31 | this.checkSyncKey = checkSyncKey;
32 | }
33 |
34 | public void setSyncKey(SyncKey syncKey) {
35 | this.syncKey = syncKey;
36 | }
37 |
38 | public void saveHotLoginData(File file){
39 | try {
40 | ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
41 | objectOutputStream.writeObject(this);
42 | objectOutputStream.flush();
43 | } catch (IOException e) {
44 | throw new RuntimeException(e);
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/WxInitInfo.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 | import com.meteor.wechatbc.entitiy.contact.Contact;
5 | import com.meteor.wechatbc.entitiy.session.SyncKey;
6 | import lombok.Data;
7 |
8 | import java.io.Serializable;
9 |
10 | @Data
11 | public class WxInitInfo implements Serializable {
12 |
13 | @JSONField(name = "SyncKey")
14 | private SyncKey syncKey;
15 |
16 | @JSONField(name = "User")
17 | private Contact user;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/ImageEmoteMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 |
5 | public class ImageEmoteMessage extends Message {
6 | @Override
7 | public String getContent() {
8 | return "(图片表情)";
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/ImageMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 | import lombok.Setter;
5 |
6 | import javax.imageio.ImageIO;
7 | import java.awt.image.BufferedImage;
8 | import java.io.ByteArrayInputStream;
9 | import java.io.File;
10 | import java.io.IOException;
11 |
12 | /**
13 | * 图片消息
14 | */
15 | public class ImageMessage extends Message {
16 |
17 | private BufferedImage bufferedImage;
18 |
19 | @Setter private byte[] bytes; // 图片的二进制信息
20 |
21 |
22 | /**
23 | * 图片二进制流转换为BufferedImage
24 | */
25 | public BufferedImage convertHexToBufferedImage() {
26 | if(bufferedImage!=null) return bufferedImage;
27 | try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes)) {
28 | return bufferedImage = ImageIO.read(bis);
29 | } catch (IOException e) {
30 | e.printStackTrace();
31 | return null;
32 | }
33 | }
34 |
35 | /**
36 | * 将图片保存至磁盘
37 | * @param file
38 | */
39 | public File saveImage(File file,String type) {
40 | try {
41 | ImageIO.write(convertHexToBufferedImage(), type, file);
42 | return file;
43 | } catch (IOException e) {
44 | e.printStackTrace();
45 | }
46 | return null;
47 | }
48 |
49 | @Override
50 | public String getContent() {
51 | return "(图片)";
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/PayMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 |
4 | import com.meteor.wechatbc.entitiy.message.Message;
5 | import lombok.Getter;
6 |
7 | import java.util.regex.Matcher;
8 | import java.util.regex.Pattern;
9 |
10 | /**
11 | * 封装二维码收款到账的消息
12 | */
13 | public class PayMessage extends Message {
14 |
15 | @Getter
16 | private double amount; // 金额
17 |
18 | @Getter
19 | private String notes; // 备注
20 |
21 | /*
22 | 提取金额信息
23 | */
24 | public void extractAmount(){
25 | String content = super.getContent();
26 | String reg = "微信支付收款([-+]?[0-9]*\\.?[0-9]+)元";
27 | Pattern compile = Pattern.compile(reg);
28 | Matcher matcher = compile.matcher(content);
29 | while (matcher.find()){
30 | this.amount = Double.parseDouble(matcher.group(1));
31 | break;
32 | }
33 | }
34 |
35 |
36 | /**
37 | * 提取备注信息
38 | */
39 | public void extractNotes(){
40 | String content = super.getContent();
41 | String reg = "
付款方备注(.+?)
";
42 | Pattern compile = Pattern.compile(reg);
43 | Matcher matcher = compile.matcher(content);
44 | while (matcher.find()){
45 | this.notes = matcher.group(1);
46 | break;
47 | }
48 | }
49 |
50 | @Override
51 | public String getContent() {
52 | return String.format("(收款信息 金额: %s 备注: %s)",amount,notes);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/RevokeMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 | import lombok.Data;
5 |
6 | import java.util.regex.Matcher;
7 | import java.util.regex.Pattern;
8 |
9 | @Data
10 | public class RevokeMessage extends Message {
11 | private Message oldMessage;
12 |
13 | /**
14 | * 获取撤回的消息ID
15 | */
16 | public String getOldMessageID(){
17 | String pattern = ";msgid>(\\d+)</";
18 | Pattern r = Pattern.compile(pattern);
19 | Matcher m = r.matcher(getContent());
20 |
21 | if (m.find()) {
22 | return m.group(1); // 提取括号中的数字部分
23 | } else {
24 | return "No oldMsgId found";
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/TextMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 |
5 | /**
6 | * 文字消息
7 | */
8 | public class TextMessage extends Message {
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/VideoMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 |
7 | import java.io.File;
8 | import java.io.FileNotFoundException;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 |
12 | public class VideoMessage extends Message {
13 |
14 | @Override
15 | public String getContent() {
16 | return "(视频消息)";
17 | }
18 |
19 | @Setter
20 | @Getter
21 | private byte[] bytes;
22 |
23 | public File saveFile(File file){
24 | try(FileOutputStream fileOutputStream = new FileOutputStream(file);
25 | ) {
26 | fileOutputStream.write(bytes);
27 | fileOutputStream.flush();
28 | return file;
29 | } catch (FileNotFoundException e) {
30 | throw new RuntimeException(e);
31 | } catch (IOException e) {
32 | throw new RuntimeException(e);
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/model/message/VoiceMessage.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.model.message;
2 |
3 | import com.meteor.wechatbc.entitiy.message.Message;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 |
7 | import java.io.File;
8 | import java.io.FileNotFoundException;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 |
12 | public class VoiceMessage extends Message {
13 | @Override
14 | public String getContent(){ return "(语音消息)";}
15 |
16 | @Setter
17 | @Getter
18 | private byte[] bytes;
19 |
20 | public File saveVoice(File file){
21 | try(FileOutputStream fileOutputStream = new FileOutputStream(file);
22 | ) {
23 | fileOutputStream.write(bytes);
24 | fileOutputStream.flush();
25 | return file;
26 | } catch (FileNotFoundException e) {
27 | throw new RuntimeException(e);
28 | } catch (IOException e) {
29 | throw new RuntimeException(e);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/plugin/BasePlugin.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.plugin;
2 |
3 |
4 | import com.meteor.wechatbc.command.WeChatCommand;
5 | import com.meteor.wechatbc.config.YamlConfiguration;
6 | import com.meteor.wechatbc.impl.WeChatClient;
7 | import com.meteor.wechatbc.plugin.Plugin;
8 | import com.meteor.wechatbc.plugin.PluginDescription;
9 | import com.meteor.wechatbc.scheduler.Scheduler;
10 | import org.apache.logging.log4j.LogManager;
11 | import org.apache.logging.log4j.Logger;
12 |
13 | import java.io.*;
14 |
15 | /**
16 | * 插件的基类
17 | */
18 | public abstract class BasePlugin implements Plugin {
19 |
20 | private boolean enable = false;
21 | private PluginDescription pluginDescription;
22 | private Logger logger;
23 |
24 | private WeChatClient weChatClient;
25 |
26 | private YamlConfiguration yamlConfiguration;
27 |
28 | /**
29 | * 初始化插件
30 | */
31 | public void init(
32 | PluginDescription pluginDescription,
33 | WeChatClient weChatClient
34 | ){
35 | this.pluginDescription = pluginDescription;
36 | this.weChatClient = weChatClient;
37 | this.logger = LogManager.getLogger(pluginDescription.getName());
38 | }
39 |
40 | public BasePlugin(){
41 | }
42 |
43 | @Override
44 | public void onDisable() {
45 | }
46 |
47 | @Override
48 | public Logger getLogger() {
49 | return logger;
50 | }
51 |
52 | public WeChatClient getWeChatClient(){
53 | return weChatClient;
54 | }
55 |
56 | public PluginDescription getPluginDescription() {
57 | return pluginDescription;
58 | }
59 |
60 | /**
61 | * 获取插件资源文件路径
62 | * @return
63 | */
64 | public File getDataFolder(){
65 | File file = new File(weChatClient.getDataFolder(),String.format("plugins/%s",getPluginDescription().getName()));
66 | if(!file.exists()) file.mkdirs();
67 | return file;
68 | }
69 |
70 | /**
71 | * 获取插件resources下的文件
72 | * @param file
73 | * @return
74 | */
75 | public InputStream getResource(String file){
76 | return getClass().getClassLoader().getResourceAsStream(file);
77 | }
78 |
79 | /**
80 | * 获取指令
81 | * @param command
82 | * @return
83 | */
84 | public WeChatCommand getCommand(String command){
85 | return weChatClient.getCommandManager().getWeChatCommandMap().get(command);
86 | }
87 |
88 |
89 | /**
90 | * 保存默认配置
91 | */
92 | public void saveDefaultConfig(){
93 | try(
94 | InputStream resource = getResource("config.yml");
95 | FileOutputStream fileOutputStream = new FileOutputStream(new File(getDataFolder(), "config.yml"));
96 | ) {
97 | byte[] bytes = new byte[resource.available()];
98 | if(bytes.length==0) return;
99 | int length = 0;
100 | while ((length = resource.read(bytes))!=-1){
101 | fileOutputStream.write(bytes,0,length);
102 | }
103 | fileOutputStream.flush();
104 | } catch (FileNotFoundException e) {
105 | throw new RuntimeException(e);
106 | } catch (IOException e) {
107 | throw new RuntimeException(e);
108 | }
109 | }
110 |
111 | /**
112 | * 重载配置文件
113 | */
114 | public void reloadConfig(){
115 | this.yamlConfiguration = YamlConfiguration.loadConfiguration(new File(getDataFolder(),"config.yml"));
116 | }
117 |
118 | /**
119 | * 获取配置文件
120 | */
121 | public YamlConfiguration getConfig(){
122 | if(yamlConfiguration==null){
123 | File file = new File(getDataFolder(), "config.yml");
124 | if(file.exists())
125 | yamlConfiguration = YamlConfiguration.loadConfiguration(file);
126 | }
127 | return yamlConfiguration;
128 | }
129 |
130 | public Scheduler getScheduler(){
131 | return getWeChatClient().getScheduler();
132 | }
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/plugin/PluginManager.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.plugin;
2 |
3 | import com.meteor.wechatbc.Main;
4 | import com.meteor.wechatbc.command.WeChatCommand;
5 | import com.meteor.wechatbc.impl.WeChatClient;
6 | import com.meteor.wechatbc.plugin.*;
7 |
8 | import java.io.File;
9 | import java.lang.reflect.InvocationTargetException;
10 | import java.net.MalformedURLException;
11 | import java.net.URL;
12 | import java.util.Map;
13 | import java.util.concurrent.ConcurrentHashMap;
14 |
15 | /**
16 | * 插件管理器
17 | */
18 | public class PluginManager {
19 |
20 | private Map pluginMap = new ConcurrentHashMap<>();
21 |
22 |
23 | private WeChatClient weChatClient;
24 |
25 | public PluginManager(WeChatClient weChatClient){
26 | this.weChatClient = weChatClient;
27 | // 创建plugins目录存放插件
28 | File pluginsFolder = new File(System.getProperty("user.dir"),"plugins");
29 | if(!pluginsFolder.exists()){
30 | pluginsFolder.mkdirs();
31 | }
32 | PluginLoader.logger.info("开始载入插件");
33 | for (File pluginFile : pluginsFolder.listFiles()) {
34 | if(pluginFile.isFile()){
35 | this.loadPlugin(pluginFile);
36 | }
37 | }
38 | PluginLoader.logger.info("载入了 {} 个插件",pluginMap.size());
39 | }
40 |
41 | /**
42 | * 卸载插件
43 | */
44 | public void unload(BasePlugin plugin){
45 | String pluginName = plugin.getPluginDescription().getName();
46 | weChatClient.getEventManager().unRegisterPluginListener(plugin);
47 | pluginMap.remove(plugin,pluginName);
48 | PluginLoader.logger.info("已卸载 {}",pluginName);
49 | }
50 |
51 | /**
52 | * 加载插件
53 | * @param file
54 | */
55 | public void loadPlugin(File file){
56 | PluginLoader pluginLoader = new PluginLoader(file);
57 | // 获取插件描述信息
58 | PluginDescription pluginDescription = pluginLoader.getPluginDescription();
59 | // 如果插件已加载,则终止后面的逻辑
60 | if(pluginMap.containsKey(pluginDescription.getName())){
61 | PluginLoader.logger.info("插件 [{}] 已存在,无法重新加载",pluginDescription.getName());
62 | return;
63 | }
64 |
65 |
66 | PluginLoader.logger.info("正在载入插件{}",pluginDescription.getName());
67 |
68 | URL[] urls = new URL[0];
69 | try {
70 | urls = new URL[]{ file.toURI().toURL() };
71 | // 为每个插件单独开一个类加载器以隔离
72 | PluginClassLoader pluginClassLoader = new PluginClassLoader(urls, Main.class.getClassLoader());
73 | // 加载插件主类
74 | Class> mainClass = Class.forName(pluginDescription.getMain(), true, pluginClassLoader);
75 | // 实例化插件主类
76 | BasePlugin plugin = (BasePlugin) mainClass.getDeclaredConstructor().newInstance();
77 | // 如果主类不是BasePlugin的子类
78 | if (!BasePlugin.class.isAssignableFrom(mainClass)) {
79 | throw new IllegalArgumentException("加载插件时发生了一个错误,主类必须继承自 BasePlugin " + pluginDescription.getMain());
80 | }
81 |
82 | if(pluginDescription.getCommands()!=null){
83 | // 解析指令并创建实例
84 | pluginDescription.getCommands().forEach(mainCommand->{
85 | WeChatCommand weChatCommand = new WeChatCommand(mainCommand);
86 | PluginLoader.logger.info(" 解析指令 {}",weChatCommand.getMainCommand());
87 | weChatClient.getCommandManager().registerCommand(weChatCommand);
88 | });
89 | }
90 | pluginMap.put(pluginDescription.getName(),plugin);
91 | // 初始化插件
92 | PluginLoader.logger.info(" 初始化 {}",pluginDescription.getMain());
93 | plugin.init(pluginDescription,weChatClient);
94 | plugin.onEnable();
95 | PluginLoader.logger.info("已载入 {}",pluginDescription.getName());
96 | } catch (Exception e){
97 | e.printStackTrace();
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/scheduler/SchedulerImpl.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.scheduler;
2 |
3 | import com.meteor.wechatbc.plugin.Plugin;
4 | import com.meteor.wechatbc.scheduler.Scheduler;
5 | import com.meteor.wechatbc.scheduler.Task;
6 |
7 | import java.util.concurrent.Executors;
8 | import java.util.concurrent.ScheduledExecutorService;
9 | import java.util.concurrent.ScheduledFuture;
10 | import java.util.concurrent.TimeUnit;
11 | import java.util.concurrent.atomic.AtomicLong;
12 |
13 | public class SchedulerImpl implements Scheduler {
14 | private final ScheduledExecutorService executor;
15 | private final AtomicLong taskIdCounter = new AtomicLong(0);
16 |
17 | public SchedulerImpl() {
18 | this.executor = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
19 | }
20 |
21 | @Override
22 | public Task runTask(Plugin plugin, Runnable task) {
23 | long taskId = taskIdCounter.incrementAndGet();
24 | executor.execute(() -> {
25 | try {
26 | task.run();
27 | } catch (Exception e) {
28 | plugin.getLogger().error("任务执行异常", e);
29 | }
30 | });
31 | return new SimpleScheduledTask(plugin,taskId);
32 | }
33 |
34 | @Override
35 | public Task runTaskLater(Plugin plugin, Runnable task, long delay) {
36 | long taskId = taskIdCounter.incrementAndGet();
37 | ScheduledFuture> future = executor.schedule(() -> {
38 | try {
39 | task.run();
40 | } catch (Exception e) {
41 | plugin.getLogger().error("延迟任务执行异常", e);
42 | }
43 | }, delay, TimeUnit.SECONDS);
44 | return new SimpleScheduledTask(plugin,taskId, future);
45 | }
46 |
47 | @Override
48 | public Task runTaskTimer(Plugin plugin, Runnable task, long delay, long period) {
49 | long taskId = taskIdCounter.incrementAndGet();
50 | ScheduledFuture> future = executor.scheduleAtFixedRate(() -> {
51 | try {
52 | task.run();
53 | } catch (Exception e) {
54 | plugin.getLogger().error("周期任务执行异常", e);
55 | }
56 | }, delay, period, TimeUnit.SECONDS);
57 | return new SimpleScheduledTask(plugin,taskId, future);
58 | }
59 |
60 | private static class SimpleScheduledTask implements Task {
61 | private final Plugin plugin;
62 | private final long taskId;
63 | private final ScheduledFuture> future;
64 |
65 | private SimpleScheduledTask(Plugin plugin,long taskId) {
66 | this.plugin = plugin;
67 | this.taskId = taskId;
68 | this.future = null;
69 | }
70 |
71 | private SimpleScheduledTask(Plugin plugin,long taskId, ScheduledFuture> future) {
72 | this.plugin = plugin;
73 | this.taskId = taskId;
74 | this.future = future;
75 | }
76 |
77 |
78 | @Override
79 | public void cancelTask() {
80 | if (future != null) {
81 | future.cancel(false);
82 | }
83 | }
84 |
85 | @Override
86 | public boolean isCancelTask() {
87 | return future != null && future.isCancelled();
88 | }
89 |
90 | @Override
91 | public Plugin getPlugin() {
92 | return plugin;
93 | }
94 |
95 | @Override
96 | public long getTaskId() {
97 | return taskId;
98 | }
99 |
100 | }
101 | }
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/synccheck/SyncCheckRunnable.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.synccheck;
2 |
3 | import com.alibaba.fastjson2.JSON;
4 | import com.alibaba.fastjson2.JSONArray;
5 | import com.alibaba.fastjson2.JSONObject;
6 | import com.github.benmanes.caffeine.cache.Cache;
7 | import com.github.benmanes.caffeine.cache.Caffeine;
8 | import com.meteor.wechatbc.impl.HttpAPI;
9 | import com.meteor.wechatbc.entitiy.contact.Contact;
10 | import com.meteor.wechatbc.entitiy.message.Message;
11 | import com.meteor.wechatbc.entitiy.session.SyncKey;
12 | import com.meteor.wechatbc.entitiy.synccheck.SyncCheckResponse;
13 | import com.meteor.wechatbc.entitiy.synccheck.SyncCheckRetcode;
14 | import com.meteor.wechatbc.impl.WeChatClient;
15 | import com.meteor.wechatbc.impl.event.EventManager;
16 | import com.meteor.wechatbc.impl.event.sub.ClientDeathEvent;
17 | import com.meteor.wechatbc.impl.event.sub.MessageEvent;
18 | import com.meteor.wechatbc.impl.event.sub.OwnerMessageEvent;
19 | import com.meteor.wechatbc.impl.event.sub.ReceiveMessageEvent;
20 | import com.meteor.wechatbc.impl.model.Session;
21 | import com.meteor.wechatbc.impl.model.message.PayMessage;
22 | import com.meteor.wechatbc.impl.model.message.VideoMessage;
23 | import com.meteor.wechatbc.impl.synccheck.message.MessageProcessor;
24 | import lombok.Getter;
25 | import org.apache.logging.log4j.LogManager;
26 | import org.apache.logging.log4j.Logger;
27 |
28 | import java.io.File;
29 | import java.util.Optional;
30 | import java.util.concurrent.ExecutorService;
31 | import java.util.concurrent.Executors;
32 | import java.util.concurrent.TimeUnit;
33 |
34 | /**
35 | * 消息检查
36 | */
37 | public class SyncCheckRunnable {
38 |
39 | private final ExecutorService executorService = Executors.newSingleThreadExecutor();
40 |
41 | private final Logger logger = LogManager.getLogger("SYNC-CHECK");
42 |
43 | public final WeChatClient weChatClient;
44 |
45 | private MessageProcessor messageProcessor;
46 |
47 | public SyncCheckRunnable(WeChatClient weChatClient){
48 | this.weChatClient = weChatClient;
49 | this.messageProcessor = new MessageProcessor(weChatClient);
50 | this.query();
51 | }
52 |
53 | /**
54 | * message缓存
55 | */
56 | @Getter private Cache messageCache =
57 | Caffeine.newBuilder().maximumSize(1000)
58 | .expireAfterWrite(30, TimeUnit.MINUTES)
59 | .build();
60 |
61 | /**
62 | * 处理消息。根据消息的类型做不同的处理 (例如转发事件)
63 | */
64 | private void handlerMessage(){
65 |
66 | HttpAPI httpAPI = weChatClient.getWeChatCore().getHttpAPI();
67 | JSONObject jsonObject = httpAPI.getMessage();
68 |
69 | Session session = weChatClient.getWeChatCore().getSession();
70 | SyncKey syncKey = JSON.toJavaObject(jsonObject.getJSONObject("SyncKey"), SyncKey.class);
71 |
72 | if(syncKey.getCount()>0){
73 | session.setSyncKey(syncKey);
74 | }
75 |
76 | SyncKey checkKey = JSON.toJavaObject(jsonObject.getJSONObject("SyncCheckKey"), SyncKey.class);
77 |
78 | if(checkKey.getCount()>0){
79 | session.setCheckSyncKey(checkKey);
80 | }
81 |
82 | JSONArray addMsgList = jsonObject.getJSONArray("AddMsgList");
83 |
84 | for (int i = 0; i < addMsgList.size(); i++) {
85 | JSONObject messageJson = addMsgList.getJSONObject(i);
86 | Message message = messageProcessor.processMessage(messageJson);
87 |
88 | weChatClient.getLogger().debug(message.toString());
89 |
90 | messageCache.put(String.valueOf(message.getMsgId()),message);
91 |
92 | String nickName = Optional.ofNullable(weChatClient.getContactManager().getContactCache().get(message.getFromUserName()))
93 | .map(Contact::getNickName)
94 | .orElse("未知");
95 |
96 | String toUser = Optional.ofNullable(weChatClient.getContactManager().getContactCache().get(message.getToUserName()))
97 | .map(Contact::getNickName).orElse("未知");
98 | logger.info("{}>{} : {}", nickName, toUser, message.getContent());
99 | callMessageEvent(new MessageEvent(messageCache.getIfPresent(String.valueOf(message.getMsgId()))));
100 | }
101 | }
102 |
103 | /**
104 | * 转播事件
105 | */
106 | private void callMessageEvent(MessageEvent messageEvent){
107 | Message message = messageEvent.getMessage();
108 | EventManager eventManager = weChatClient.getEventManager();
109 |
110 | eventManager.callEvent(messageEvent);
111 |
112 | Session session = weChatClient.getWeChatCore().getSession();
113 | Contact owner = session.getWxInitInfo().getUser();
114 |
115 | if(message.getFromUserName().equalsIgnoreCase(owner.getUserName())){
116 | // 本人发出消息
117 | eventManager.callEvent(new OwnerMessageEvent(message));
118 | }else {
119 | // 接收消息
120 | eventManager.callEvent(new ReceiveMessageEvent(message));
121 | }
122 | }
123 |
124 | /**
125 | * 轮询获取新的消息状态
126 | */
127 | public void query(){
128 | executorService.submit(()->{
129 | HttpAPI httpAPI = weChatClient.getWeChatCore().getHttpAPI();
130 | try {
131 | SyncCheckResponse syncCheckResponse = httpAPI.syncCheck();
132 | if(syncCheckResponse.getSyncCheckRetcode() == SyncCheckRetcode.NORMAL){
133 | handlerMessage();
134 | }else {
135 | weChatClient.getEventManager().callEvent(new ClientDeathEvent(weChatClient,syncCheckResponse.getSyncCheckSelector()));
136 | logger.error(syncCheckResponse.getSyncCheckRetcode().getMessage());
137 | }
138 | }catch (Exception e){
139 | e.printStackTrace();
140 | logger.info("在尝试异步获取消息时遇到了一个错误");
141 | }
142 | this.query();
143 | });
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/synccheck/message/ConcreteMessageFactory.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.synccheck.message;
2 |
3 | import com.alibaba.fastjson2.JSON;
4 | import com.alibaba.fastjson2.JSONObject;
5 | import com.meteor.wechatbc.entitiy.message.Message;
6 | import com.meteor.wechatbc.impl.WeChatClient;
7 | import com.meteor.wechatbc.impl.model.MsgType;
8 | import com.meteor.wechatbc.impl.model.message.*;
9 |
10 | import java.io.File;
11 |
12 |
13 | public class ConcreteMessageFactory implements MessageFactory {
14 |
15 | private final WeChatClient weChatClient;
16 |
17 | public ConcreteMessageFactory(WeChatClient weChatClient){
18 | this.weChatClient = weChatClient;
19 | }
20 |
21 |
22 |
23 |
24 |
25 | /**
26 | * 根据响应的格式获取message的子类
27 | * @param messageJson
28 | * @return
29 | */
30 | @Override
31 | public Message createMessage(JSONObject messageJson) {
32 | Message message = JSON.toJavaObject(messageJson, Message.class);
33 | MsgType msgType = message.getMsgType();
34 |
35 | if(msgType == MsgType.TextMsg){
36 | return JSON.toJavaObject(messageJson, TextMessage.class);
37 | }else if(msgType == MsgType.ImageMsg){
38 | ImageMessage imageMessage = JSON.toJavaObject(messageJson, ImageMessage.class);
39 | imageMessage.setBytes(weChatClient.getWeChatCore().getHttpAPI().getMsgImage(String.valueOf(message.getMsgId()))); // 设置图片原始数据
40 | return imageMessage;
41 | }else if(msgType == MsgType.ImgEmoteMsg){
42 | return JSON.toJavaObject(messageJson, ImageEmoteMessage.class);
43 | }else if(msgType==MsgType.VideoMsg){
44 | VideoMessage videoMessage = JSON.toJavaObject(messageJson, VideoMessage.class);
45 | videoMessage.setBytes(weChatClient.getWeChatCore().getHttpAPI().getVideo(videoMessage.getMsgId()));
46 | return videoMessage;
47 | }else if(msgType==MsgType.RevokeMsg){
48 | RevokeMessage revokeMessage = JSON.toJavaObject(messageJson, RevokeMessage.class);
49 | revokeMessage.setOldMessage(weChatClient.getSyncCheckRunnable().getMessageCache().getIfPresent(revokeMessage.getOldMessageID()));
50 | return revokeMessage;
51 | }else if(msgType==MsgType.APPMsg){
52 | if(message.getContent().contains("收款")){
53 | PayMessage payMessage = JSON.toJavaObject(messageJson,PayMessage.class);
54 | payMessage.extractAmount();
55 | payMessage.extractNotes();
56 | return payMessage;
57 | }
58 | }else if(msgType==MsgType.VoiceMsg){
59 | VoiceMessage voiceMessage = JSON.toJavaObject(messageJson, VoiceMessage.class);
60 | voiceMessage.setBytes(weChatClient.getWeChatCore().getHttpAPI().getVoice(voiceMessage.getMsgId()));
61 | voiceMessage.saveVoice(new File(weChatClient.getDataFolder(), "voice/" + voiceMessage.getMsgId() + ".mp3"));
62 | return voiceMessage;
63 | }
64 | return message;
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/synccheck/message/MessageFactory.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.synccheck.message;
2 |
3 | import com.alibaba.fastjson2.JSONObject;
4 | import com.meteor.wechatbc.entitiy.message.Message;
5 |
6 | public interface MessageFactory {
7 | Message createMessage(JSONObject message);
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/impl/synccheck/message/MessageProcessor.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.impl.synccheck.message;
2 |
3 | import com.alibaba.fastjson2.JSONObject;
4 | import com.meteor.wechatbc.entitiy.message.Message;
5 | import com.meteor.wechatbc.impl.WeChatClient;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * 消息工厂
11 | * 用于将原始message转换为 图片消息,文本消息等
12 | */
13 | public class MessageProcessor {
14 |
15 | private final ConcreteMessageFactory concreteMessageFactory;
16 |
17 | public MessageProcessor(WeChatClient weChatClient){
18 | this.concreteMessageFactory = new ConcreteMessageFactory(weChatClient);
19 | }
20 |
21 |
22 | public Message processMessage(JSONObject jsonObject) {
23 | return concreteMessageFactory.createMessage(jsonObject);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/launch/login/DefaultPrintQRCodeCallBack.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.launch.login;
2 |
3 | import com.meteor.wechatbc.launch.login.PrintQRCodeCallBack;
4 | import com.meteor.wechatbc.util.VersionCheck;
5 |
6 | public class DefaultPrintQRCodeCallBack implements PrintQRCodeCallBack {
7 | @Override
8 | public String print(String uuid) {
9 | String url = "https://login.weixin.qq.com/qrcode/"+uuid;
10 | System.out.println("访问: "+url+" 进行登录!");
11 | return null;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/launch/login/PrintQRCodeCallBack.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.launch.login;
2 |
3 | import java.util.UUID;
4 |
5 | /**
6 | * 打印登录二维码的回调
7 | */
8 | public interface PrintQRCodeCallBack {
9 | String print(String uuid);
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/launch/login/WeChatLogin.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.launch.login;
2 |
3 | import com.meteor.wechatbc.entitiy.session.BaseRequest;
4 | import com.meteor.wechatbc.launch.login.cokkie.WeChatCookieJar;
5 | import com.meteor.wechatbc.launch.login.model.LoginMode;
6 | import com.meteor.wechatbc.launch.login.model.QRCodeResponse;
7 | import com.meteor.wechatbc.util.BaseConfig;
8 | import com.meteor.wechatbc.util.HttpUrlHelper;
9 | import com.meteor.wechatbc.util.URL;
10 | import okhttp3.*;
11 | import org.apache.logging.log4j.Logger;
12 |
13 | import java.io.IOException;
14 | import java.util.ArrayList;
15 | import java.util.HashMap;
16 | import java.util.List;
17 | import java.util.Map;
18 | import java.util.concurrent.TimeUnit;
19 |
20 | import static com.meteor.wechatbc.util.URL.*;
21 |
22 | /**
23 | * 扫码登陆微信
24 | */
25 | public class WeChatLogin {
26 | private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
27 | .cookieJar(new WeChatCookieJar())
28 | .connectTimeout(30, TimeUnit.MINUTES)
29 | .readTimeout(30,TimeUnit.MINUTES).
30 | build();
31 |
32 | private final HttpUrl.Builder urlBuilder = HttpUrl.parse(LOGINJS).newBuilder();
33 |
34 | private Logger logger;
35 |
36 | private boolean login;
37 |
38 |
39 | public WeChatLogin(Logger logger){
40 | this.logger = logger;
41 | }
42 |
43 | // 获取登陆UUID
44 | public String getLoginUUID(){
45 | Map queryParams = new HashMap<>();
46 | queryParams.put("mod", "desktop");
47 |
48 | HttpUrl redirectUrl = HttpUrl.parse(NEWLOGINPAGE)
49 | .newBuilder()
50 | .encodedQuery(HttpUrlHelper.encodeParams(queryParams)) // Encode query parameters
51 | .build();
52 |
53 | queryParams.clear();
54 | queryParams.put("redirect_uri", redirectUrl.toString());
55 | queryParams.put("appid", BaseConfig.APP_ID);
56 | queryParams.put("fun", "new");
57 | queryParams.put("lang", "zh_CN");
58 | queryParams.put("_", String.valueOf(System.currentTimeMillis()));
59 |
60 | for (Map.Entry entry : queryParams.entrySet()) {
61 | urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
62 | }
63 |
64 | Request request = new Request.Builder()
65 | .url(urlBuilder.build())
66 | .get()
67 | .addHeader("User-Agent","Mozilla/5.0 (Linux; U; UOS x86_64; zh-cn) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 UOSBrowser/6.0.1.1001")
68 | .build();
69 |
70 | try {
71 | return HttpUrlHelper.getValueByKey(HttpUrlHelper.okHttpClient.newCall(request).execute(),"window.QRLogin.uuid");
72 | } catch (IOException e) {
73 | throw new RuntimeException(e);
74 | }
75 | }
76 |
77 | /**
78 | * 检查扫码状态
79 | */
80 | private QRCodeResponse checkLogin(String uuid, String tip){
81 | long now = System.currentTimeMillis() / 1000;
82 | String queryString = String.format("r=%d&_=%d&loginicon=true&uuid=%s&tip=%s",
83 | now / 1579, now, uuid, tip);
84 | String fullURL = LOGIN + "?" + queryString;
85 | Request request = new Request.Builder()
86 | .url(fullURL)
87 | .get().build();
88 | try(Response response = okHttpClient.newCall(request).execute()) {
89 | return new QRCodeResponse(response.body().string());
90 | } catch (IOException e) {
91 | throw new RuntimeException(e);
92 | }
93 | }
94 |
95 | private BaseRequest baseRequest = null;
96 |
97 | public BaseRequest getLoginInfo(){
98 | return baseRequest;
99 | }
100 |
101 | /**
102 | * 从Cookie中获取登录信息
103 | * @return
104 | */
105 | private BaseRequest getLoginInfo(QRCodeResponse qrCodeResponse) {
106 | if(baseRequest!=null) return baseRequest;
107 | Request request = new Request.Builder()
108 | .url(qrCodeResponse.getUrl() + "&fun=new")
109 | .addHeader("version", "2.0.0")
110 | .addHeader("extspam", BaseConfig.EXTSPAM)
111 | .addHeader("Content-Type", "application/x-www-form-urlencoded")
112 | .build();
113 | try (Response execute = okHttpClient.newCall(request).execute()) {
114 | BaseRequest baseRequest = new BaseRequest(execute.body().string());
115 | List cookies = okHttpClient.cookieJar().loadForRequest(request.url());
116 | baseRequest.setInitCookie(cookies);
117 | // 设置登录设备ID
118 | baseRequest.setDeviceId(BaseConfig.getDeviceId());
119 |
120 | String baseDomain = "wx.qq.com";
121 | if (!cookies.isEmpty()) {
122 | Cookie ck = cookies.get(0);
123 | baseDomain = ck.domain();
124 | }
125 |
126 | // 获取cookie的 domain 所属域名
127 | URL.setBASE_URL(new HttpUrl.Builder()
128 | .scheme("https")
129 | .host(baseDomain)
130 | .build());
131 |
132 | for (Cookie cookie : cookies) {
133 | String name = cookie.name();
134 | if (name.startsWith("webwx_data_ticket")) {
135 | baseRequest.setDataTicket(cookie.value());
136 | } else if (name.startsWith("webwx_auth_ticket")) {
137 | baseRequest.setAuthTicket(cookie.value());
138 | }
139 | }
140 | return baseRequest;
141 | } catch (IOException e) {
142 | throw new RuntimeException(e);
143 | }
144 | }
145 |
146 |
147 | public void waitLogin(String uuid){
148 | QRCodeResponse qrCodeResponse = null;
149 | while (!login){
150 | qrCodeResponse = checkLogin(uuid, "0");
151 | logger.info(qrCodeResponse.getLoginMode().getMsg());
152 | login = qrCodeResponse.getLoginMode()== LoginMode.LOGIN_MODE200;
153 | }
154 | // 获取登陆信息
155 | this.baseRequest = getLoginInfo(qrCodeResponse);
156 | logger.info("已取得登录信息:");
157 | logger.info(baseRequest.toString());
158 | }
159 |
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/launch/login/cokkie/WeChatCookieJar.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.launch.login.cokkie;
2 |
3 | import okhttp3.Cookie;
4 | import okhttp3.CookieJar;
5 | import okhttp3.HttpUrl;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.concurrent.ConcurrentHashMap;
11 |
12 | /**
13 | * 在登陆的时候提取token存储
14 | */
15 | public class WeChatCookieJar implements CookieJar {
16 | private Map> cookieMap;
17 |
18 | public WeChatCookieJar(){
19 | this.cookieMap = new ConcurrentHashMap<>();
20 | }
21 |
22 | @Override
23 | public void saveFromResponse(HttpUrl httpUrl, List list) {
24 | cookieMap.put(httpUrl,list);
25 | }
26 |
27 | @Override
28 | public List loadForRequest(HttpUrl httpUrl) {
29 | return cookieMap.getOrDefault(httpUrl,new ArrayList<>());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/launch/login/model/LoginMode.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.launch.login.model;
2 |
3 | public enum LoginMode{
4 | LOGIN_MODE408(401,"等待扫码中..."),
5 | LOGIN_MODE200(200,"已确认登陆"),
6 | LOGIN_MODE201(201,"已扫描二维码,请确认登陆");
7 | private Integer code;
8 | private String msg;
9 | LoginMode(Integer code,String msg){
10 | this.code = code;
11 | this.msg = msg;
12 | }
13 | public Integer getCode() {
14 | return code;
15 | }
16 |
17 | public String getMsg() {
18 | return msg;
19 | }
20 | }
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/launch/login/model/QRCodeResponse.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.launch.login.model;
2 |
3 | import com.meteor.wechatbc.util.HttpUrlHelper;
4 | import lombok.Data;
5 |
6 | /**
7 | * 扫码响应信息
8 | */
9 | @Data
10 | public class QRCodeResponse {
11 | private LoginMode loginMode;
12 | private String url;
13 |
14 | public QRCodeResponse(String data){
15 | String valueByKey = HttpUrlHelper.getValueByKey(data, "window.code");
16 | if(valueByKey.equalsIgnoreCase("201")){
17 | loginMode = LoginMode.LOGIN_MODE201;
18 | }else if(valueByKey.equalsIgnoreCase("408")) loginMode = LoginMode.LOGIN_MODE408;
19 | else loginMode = LoginMode.LOGIN_MODE200;
20 | this.url = HttpUrlHelper.getValueByKey(data,"window.redirect_uri");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/plugin/Plugin.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.plugin;
2 |
3 |
4 | import org.apache.logging.log4j.Logger;
5 |
6 | /**
7 | * 描述一个wechatbc插件
8 | */
9 | public interface Plugin {
10 |
11 | Logger getLogger();
12 |
13 | void onLoad();
14 |
15 | void onEnable();
16 |
17 | void onDisable();
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/plugin/PluginClassLoader.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.plugin;
2 |
3 | import java.net.URL;
4 | import java.net.URLClassLoader;
5 |
6 | public class PluginClassLoader extends URLClassLoader {
7 | private ClassLoader parentClassLoader;
8 |
9 | public PluginClassLoader(URL[] urls, ClassLoader parentClassLoader) {
10 | super(urls, parentClassLoader);
11 | this.parentClassLoader = parentClassLoader;
12 | }
13 |
14 | @Override
15 | protected Class> findClass(String name) throws ClassNotFoundException {
16 | try {
17 | // 首先尝试插件类加载器加载类
18 | return super.findClass(name);
19 | } catch (ClassNotFoundException e) {
20 | // 如果失败,尝试主应用类加载器加载类
21 | return parentClassLoader.loadClass(name);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/plugin/PluginDescription.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.plugin;
2 |
3 | import lombok.Data;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * 如bukkit插件中的plugin.yml一样描述一个插件
9 | */
10 | @Data
11 | public class PluginDescription {
12 | private String name;
13 | private String version;
14 | private List authors;
15 | private String description;
16 | private String main;
17 | private List commands;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/plugin/PluginLoader.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.plugin;
2 |
3 | import lombok.Getter;
4 | import org.apache.logging.log4j.LogManager;
5 | import org.apache.logging.log4j.Logger;
6 | import org.yaml.snakeyaml.Yaml;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.util.jar.JarEntry;
12 | import java.util.jar.JarFile;
13 |
14 | public class PluginLoader {
15 |
16 | private final File file;
17 |
18 | public static Logger logger = LogManager.getLogger("plugin-loader");
19 |
20 | @Getter private PluginDescription pluginDescription;
21 |
22 | public PluginLoader(File file){
23 | this.file = file;
24 | this.loadPlugin();
25 | }
26 | /**
27 | * 获得BasePlugin实例
28 | */
29 | public void loadPlugin(){
30 | try (JarFile jarFile = new JarFile(file)) {
31 | this.pluginDescription = pluginDescription(jarFile);
32 | } catch (Exception e) {
33 | e.printStackTrace();
34 | }
35 | }
36 |
37 | /**
38 | * 取得 PluginDescription
39 | */
40 | private PluginDescription pluginDescription(JarFile jarFile){
41 | JarEntry entry = jarFile.getJarEntry("plugin.yml");
42 | if (entry != null) {
43 | try (InputStream input = jarFile.getInputStream(entry)) {
44 | Yaml yaml = new Yaml();
45 | return yaml.loadAs(input, PluginDescription.class);
46 | } catch (IOException e) {
47 | throw new RuntimeException(e);
48 | }
49 | } else {
50 | logger.info("尝试载入的插件不存在 plugin.yml!!");
51 | }
52 | return null;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/scheduler/Scheduler.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.scheduler;
2 |
3 | import com.meteor.wechatbc.plugin.Plugin;
4 |
5 | /**
6 | * 任务调度器
7 | */
8 | public interface Scheduler {
9 |
10 |
11 | /**
12 | * 在指定的延迟后执行一次性任务。
13 | *
14 | * @param plugin 插件实例
15 | * @param task 要执行的任务
16 | * @param delay 延迟(秒)
17 | * @return 调度的任务
18 | */
19 | Task runTaskLater(Plugin plugin, Runnable task,long delay);
20 |
21 | /**
22 | * 以固定的周期执行任务。
23 | *
24 | * @param plugin 插件实例
25 | * @param task 要执行的任务
26 | * @param delay 初始延迟(秒)
27 | * @param period 执行周期(秒)
28 | * @return 调度的任务
29 | */
30 | Task runTaskTimer(Plugin plugin, Runnable task, long delay, long period);
31 |
32 |
33 | /**
34 | * 立即执行一个任务。
35 | *
36 | * @param plugin 插件实例
37 | * @param task 要执行的任务
38 | * @return 调度的任务
39 | */
40 | Task runTask(Plugin plugin, Runnable task);
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/scheduler/Task.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.scheduler;
2 |
3 | import com.meteor.wechatbc.plugin.Plugin;
4 |
5 | /**
6 | * 调度任务类
7 | */
8 | public interface Task {
9 |
10 | /**
11 | * 持有该任务的插件
12 | * @return
13 | */
14 | Plugin getPlugin();
15 |
16 | /**
17 | * 检查任务是否已取消
18 | */
19 | boolean isCancelTask();
20 |
21 | /**
22 | * 获取任务的唯一标识
23 | */
24 | long getTaskId();
25 |
26 | /**
27 | * 取消任务
28 | */
29 | void cancelTask();
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/scheduler/WeChatRunnable.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.scheduler;
2 |
3 | import com.meteor.wechatbc.impl.plugin.BasePlugin;
4 |
5 | public abstract class WeChatRunnable implements Runnable{
6 | private Task task;
7 |
8 | public Task runTaskLater(BasePlugin plugin, long delay){
9 | this.check();
10 | task = plugin.getScheduler().runTaskLater(plugin,this,delay);
11 | return task;
12 | }
13 |
14 | public Task runTaskTimer(BasePlugin plugin,long delay,long period){
15 | this.check();
16 | task = plugin.getScheduler().runTaskTimer(plugin,this,delay,period);
17 | return task;
18 | }
19 |
20 | public Task runTask(BasePlugin plugin){
21 | this.check();
22 | task = plugin.getScheduler().runTask(plugin,this);
23 | return task;
24 | }
25 |
26 | private void check(){
27 | if(task!=null){
28 | throw new IllegalStateException("This runnable has already scheduled");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/util/BaseConfig.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.util;
2 |
3 | import java.util.Random;
4 |
5 | /**
6 | * 接口访问固定的信息
7 | */
8 | public class BaseConfig {
9 |
10 | public static final String APP_ID = "wx782c26e4c19acffb";
11 |
12 | public static final String EXTSPAM = "Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykC" +
13 | "yNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R" +
14 | "3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5p" +
15 | "M7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHa" +
16 | "GGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYSc" +
17 | "W8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8" +
18 | "wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn" +
19 | "2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYIT" +
20 | "IqItIKjD35IGKAUwAA==";
21 |
22 | public static final String USER_AGENT = "Mozilla/5.0 (Linux; U; UOS x86_64; zh-cn) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 UOSBrowser/6.0.1.1001";
23 |
24 | // 生成设备ID
25 | public static String getDeviceId(){
26 | StringBuilder builder = new StringBuilder(16);
27 | builder.append("e");
28 | Random random = new Random();
29 | for (int i = 0; i < 15; i++) { //
30 | int r = random.nextInt(10);
31 | builder.append(r);
32 | }
33 |
34 | return builder.toString();
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/util/HttpUrlHelper.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.util;
2 |
3 | import okhttp3.OkHttpClient;
4 | import okhttp3.Response;
5 |
6 | import java.io.IOException;
7 | import java.util.Map;
8 | import java.util.Random;
9 |
10 | public class HttpUrlHelper {
11 |
12 | public static OkHttpClient okHttpClient = new OkHttpClient().newBuilder().build();
13 |
14 | public static String getValueByKey(Response response,String key){
15 | try {
16 | return getValueByKey(response.body().string(),key);
17 | } catch (IOException e) {
18 | throw new RuntimeException(e);
19 | }
20 | }
21 |
22 | public static String getValueByKey(String response, String key) {
23 | String[] keyValuePairs = response.split(";");
24 | for (String pair : keyValuePairs) {
25 | String[] parts = pair.trim().split("=", 2);
26 | if (parts.length == 2) {
27 | String currentKey = parts[0].trim();
28 | String value = parts[1].trim();
29 | if (key.equals(currentKey)) {
30 | if (value.startsWith("\"") && value.endsWith("\"")) {
31 | return value.substring(1, value.length() - 1);
32 | } else {
33 | return value;
34 | }
35 | }
36 | }
37 | }
38 | return null;
39 | }
40 |
41 | public static String generateTimestampWithRandom() {
42 | return System.currentTimeMillis() / 1000 + random(6);
43 | }
44 |
45 | public static String random(int count) {
46 | RandomString gen = new RandomString(count, new Random());
47 | return gen.nextString();
48 | }
49 |
50 | public static String encodeParams(Map params) {
51 | StringBuilder encodedParams = new StringBuilder();
52 | for (Map.Entry entry : params.entrySet()) {
53 | if (encodedParams.length() > 0) {
54 | encodedParams.append("&");
55 | }
56 | encodedParams.append(entry.getKey())
57 | .append("=")
58 | .append(entry.getValue());
59 | }
60 | return encodedParams.toString();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/util/RandomString.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.util;
2 |
3 | import java.security.SecureRandom;
4 | import java.util.Locale;
5 | import java.util.Random;
6 |
7 | public class RandomString {
8 |
9 | /**
10 | * Generate a random string.
11 | */
12 | public String nextString() {
13 | for (int idx = 0; idx < buf.length; ++idx) {
14 | buf[idx] = symbols[random.nextInt(symbols.length)];
15 | }
16 | return new String(buf);
17 | }
18 |
19 | public static final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
20 |
21 | public static final String lower = upper.toLowerCase(Locale.ROOT);
22 |
23 | public static final String digits = "0123456789";
24 |
25 | public static final String alphanum = upper + lower + digits;
26 |
27 | private final Random random;
28 |
29 | private final char[] symbols;
30 |
31 | private final char[] buf;
32 |
33 | public RandomString(int length, Random random, String symbols) {
34 | if (length < 1) throw new IllegalArgumentException();
35 | if (symbols.length() < 2) throw new IllegalArgumentException();
36 | this.random = random;
37 | this.symbols = symbols.toCharArray();
38 | this.buf = new char[length];
39 | }
40 |
41 | /**
42 | * Create an alphanumeric string generator.
43 | */
44 | public RandomString(int length, Random random) {
45 | this(length, random, alphanum);
46 | }
47 |
48 | /**
49 | * Create an alphanumeric strings from a secure generator.
50 | */
51 | public RandomString(int length) {
52 | this(length, new SecureRandom());
53 | }
54 |
55 | /**
56 | * Create session identifiers.
57 | */
58 | public RandomString() {
59 | this(21);
60 | }
61 |
62 | }
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/util/URL.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.util;
2 |
3 | import lombok.Setter;
4 | import okhttp3.HttpUrl;
5 |
6 | /**
7 | * 微信相关接口
8 | */
9 | public class URL {
10 |
11 |
12 |
13 | @Setter public static HttpUrl BASE_URL = new HttpUrl.Builder()
14 | .scheme("https")
15 | .host("wx.qq.com")
16 | .build();
17 |
18 |
19 |
20 | public final static String WXINIT = "/cgi-bin/mmwebwx-bin/webwxinit";
21 | public final static String SYNCCHECK = "/cgi-bin/mmwebwx-bin/synccheck";
22 |
23 | public final static String WEBWXSYNC = "/cgi-bin/mmwebwx-bin/webwxsync";
24 |
25 | public final static String SEND_MESSAGE = "/cgi-bin/mmwebwx-bin/webwxsendmsg";
26 | public final static String GET_CONTACT = "/cgi-bin/mmwebwx-bin/webwxgetcontact";
27 | public final static String BATCH_GET_CONTACT = "/cgi-bin/mmwebwx-bin/webwxbatchgetcontact";
28 |
29 | public final static String LOGINJS = "https://login.wx.qq.com/jslogin";
30 | public final static String NEWLOGINPAGE = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage";
31 | public final static String LOGIN = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login";
32 |
33 | public final static String GET_VIDEO = "/cgi-bin/mmwebwx-bin/webwxgetvideo";
34 |
35 | public final static String GET_VOICE = "/cgi-bin/mmwebwx-bin/webwxgetvoice";
36 | public final static String GET_MSG_IMG = "/cgi-bin/mmwebwx-bin/webwxgetmsgimg";
37 | public final static String REVOKE = "/cgi-bin/mmwebwx-bin/webwxrevokemsg";
38 |
39 | public final static String UPLOAD_FILE = "/cgi-bin/mmwebwx-bin/webwxuploadmedia";
40 |
41 | public final static String SEND_IMAGE = "/cgi-bin/mmwebwx-bin/webwxsendmsgimg";
42 |
43 | public final static String SEND_VIDEO = "/cgi-bin/mmwebwx-bin/webwxsendvideomsg";
44 |
45 | public final static String GET_ICON = "/cgi-bin/mmwebwx-bin/webwxgeticon";
46 | }
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/util/VersionCheck.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.util;
2 |
3 | import com.alibaba.fastjson2.JSON;
4 | import com.meteor.wechatbc.util.mode.GitHubRelease;
5 | import okhttp3.OkHttpClient;
6 | import okhttp3.Request;
7 | import okhttp3.Response;
8 |
9 | import java.io.IOException;
10 |
11 | public class VersionCheck {
12 |
13 | private static final String CURRENT_VERSION = "v1.2";
14 |
15 | public static void check(String owner, String repo) {
16 |
17 | OkHttpClient client = new OkHttpClient();
18 | String url = "https://api.github.com/repos/" + owner + "/" + repo + "/releases/latest";
19 | System.out.println("当前版本: "+CURRENT_VERSION);
20 | Request request = new Request.Builder()
21 | .url(url)
22 | .build();
23 |
24 | try (Response response = client.newCall(request).execute()) {
25 | if (!response.isSuccessful()) {
26 | throw new IOException("Unexpected code " + response);
27 | }
28 | GitHubRelease gitHubRelease = JSON.parseObject(response.body().string(), GitHubRelease.class);
29 | if(!gitHubRelease.getTagName().equalsIgnoreCase(CURRENT_VERSION)){
30 | System.out.println("检测到新版本: "+gitHubRelease.getTagName());
31 | System.out.println("详情: ");
32 | System.out.println(gitHubRelease.getBody());
33 | System.out.println("地址: https://github.com/meteorOSS/wechat-bc/releases/latest");
34 | System.out.println();
35 | }
36 | } catch (IOException e) {
37 | e.printStackTrace();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/meteor/wechatbc/util/mode/GitHubRelease.java:
--------------------------------------------------------------------------------
1 | package com.meteor.wechatbc.util.mode;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 | import lombok.Data;
5 |
6 | @Data
7 | public class GitHubRelease {
8 | @JSONField(name = "tag_name")
9 | private String tagName;
10 | private String name;
11 | private String body;
12 | }
--------------------------------------------------------------------------------
/src/main/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d{yyyy-MM-dd HH:mm:ss} [%highlight{[%-5level]}{FATAL=bright red, ERROR=red, WARN=yellow, INFO=white, DEBUG=blue, TRACE=white}] [%logger] : %msg%n
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------