├── .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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | 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 | ![image](https://github.com/meteorOSS/WeChatBc/assets/61687266/86f34b62-5f5b-4a3d-a3cc-cc151606b495) 12 | 13 | ![image](https://github.com/meteorOSS/WeChatBc/assets/61687266/dc4bce02-e5c2-416f-9f90-312f1004b9b0) 14 | 15 | (图片效果需安装 [WeChatSetu插件](https://github.com/meteorOSS/WeChatSetu) ) 16 | 17 | ![image](https://github.com/meteorOSS/wechat-bc/assets/61687266/a5cde024-318d-4c04-b87b-7f56bc7fafa3) 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 | [![wechatbc](https://api.star-history.com/svg?repos=meteorOSS/wechat-bc&type=Date)](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 | --------------------------------------------------------------------------------