├── .gitignore ├── Get_Bot_Token.md ├── Migrate_To_Latest.md ├── README.md ├── ch_0 └── README.md ├── ch_1 ├── README.md ├── code │ ├── .gitignore │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── snw │ │ │ └── jkook │ │ │ └── example │ │ │ └── Main.java │ │ └── resources │ │ └── plugin.yml └── images │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ └── 7.png ├── ch_10 ├── README.md └── code │ ├── .gitignore │ ├── README.md │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── snw │ │ └── jkook │ │ └── example │ │ ├── EventListeners.java │ │ ├── GuildChannelStorage.java │ │ ├── Main.java │ │ ├── PermissionStorage.java │ │ ├── Scoreboard.java │ │ ├── ScoreboardStorage.java │ │ ├── commands │ │ ├── ActiveCommand.java │ │ ├── RankCommand.java │ │ ├── RankListCommand.java │ │ └── SetScoreCommand.java │ │ ├── events │ │ └── LevelUpEvent.java │ │ └── math │ │ ├── MathEventContainer.java │ │ ├── MathEventPublisher.java │ │ └── RandomMath.java │ └── resources │ └── plugin.yml ├── ch_2 ├── README.md └── code │ ├── .gitignore │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── snw │ │ └── jkook │ │ └── example │ │ └── Main.java │ └── resources │ ├── config.yml │ └── plugin.yml ├── ch_3 ├── README.md └── code │ ├── .gitignore │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── snw │ │ └── jkook │ │ └── example │ │ └── Main.java │ └── resources │ ├── config.yml │ ├── hello.txt │ └── plugin.yml ├── ch_4 ├── README.md ├── code │ ├── .gitignore │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── snw │ │ │ └── jkook │ │ │ └── example │ │ │ └── Main.java │ │ └── resources │ │ └── plugin.yml └── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── ch_5 ├── README.md ├── code │ ├── .gitignore │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── snw │ │ │ └── jkook │ │ │ └── example │ │ │ └── Main.java │ │ └── resources │ │ └── plugin.yml └── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── ch_6 ├── README.md └── code │ ├── .gitignore │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── snw │ │ └── jkook │ │ └── example │ │ └── Main.java │ └── resources │ └── plugin.yml ├── ch_7 ├── README.md └── code │ ├── .gitignore │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── snw │ │ └── jkook │ │ └── example │ │ ├── EventListener.java │ │ └── Main.java │ └── resources │ └── plugin.yml ├── ch_8 ├── README.md └── code │ ├── .gitignore │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── snw │ │ └── jkook │ │ └── example │ │ └── Main.java │ └── resources │ └── plugin.yml └── ch_9 ├── README.md └── code ├── .gitignore ├── pom.xml └── src └── main ├── java └── snw │ └── jkook │ └── example │ ├── AnotherDataObj.java │ ├── DataObj.java │ └── Main.java └── resources ├── config.yml └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | 115 | .vscode/ 116 | -------------------------------------------------------------------------------- /Get_Bot_Token.md: -------------------------------------------------------------------------------- 1 | # 附录: 获取一个 KOOK 的机器人 Token 2 | 3 | 开发 KOOK 的机器人,当然要先有一个机器人账号啦,获取机器人账号也很简单,按以下方法操作即可。 4 | 5 | 首先,你要有一个 KOOK 账号。 6 | 7 | 来到 [KOOK 开发者中心 - 应用后台](https://developer.kookapp.cn/app/index) ,登录你的 KOOK 账号。 8 | 9 | 之后在跳转到的新页面中间,偏右下角的地方找到一个 "授权" 按钮,点击。 10 | 11 | 然后浏览器会跳转到 KOOK 的应用后台,点击页面右上角的 "新建应用" 按钮,在弹出的对话框中输入新机器人的名字,按确定。 12 | 13 | 这样,Bot 就创建好了。 14 | 15 | 然后你将会回到应用后台,点击创建好的机器人应用头像。 16 | 17 | 现在,你能在左侧的「设置」选项卡列表中看到一项「机器人」,点开它。 18 | 19 | 点开「机器人」选项卡后,就能看到机器人的信息以及「机器人连接模式」、「Token」。 20 | 21 | 「机器人连接模式」请保持为「websocket」。这个连接模式便于你在你的设备上调试你的机器人,在你的机器人用户量不大时很推荐。 22 | 23 | 本教程不会讲 Webhook 模式的使用。如果你是 KookBC (一个 JKook API 的标准实现) 的用户,并且需要此模式,可以看 [KookBC 与 Webhook](https://github.com/SNWCreations/KookBC/blob/main/docs/KookBC_with_Webhook.md) 配置文档。 24 | 25 | 「Token」是机器人的身份凭证,请不要: 26 | 27 | * 上传到任何地方 28 | * 告诉其他人 29 | * 写进将被公开的代码 30 | 31 | 若有不慎泄露(如在开发时遇到问题需要求助却不小心在截图中包含了没有打码的 Token),在后台的 Token 框右侧有 "重置" 按钮,请毫不犹豫的按下去,然后用新 Token 替换你环境中的旧 Token。 32 | 33 | --- 34 | 35 | 本文编写时参考了 [khl.py Example - 引言](https://github.com/TWT233/khl.py/blob/main/example/README.md) ,在此表示感谢。 36 | -------------------------------------------------------------------------------- /Migrate_To_Latest.md: -------------------------------------------------------------------------------- 1 | # 从 API 0.37 迁移到最新 API 2 | 3 | 最新的版本有最新的修补与功能。 4 | 5 | 这句话对 JKook API 也适用。 6 | 7 | 虽然 API 0.37 是 LTS 的(它曾经是,LTS 也早已到期),但是我们不会永远专注于 API 0.37 ,我们把开发资源优先提供给更新的版本。 8 | 9 | 而且,迁移能让你得到更多的功能。 10 | 11 | ## 为什么会这样? 12 | 13 | JKook API 有一条设计原则: 尽量向下兼容。 14 | 15 | 但是,在 API 0.38 中发生了与这条原则不符的事情: 我们重构了命令系统,加入了一些新功能,因此推翻了旧代码。 16 | 17 | 所以你在 API 0.37 上编写的命令直接拿到 API 0.48 的实现上是无法运行的。 18 | 19 | ## 两个 API 版本之间不兼容的内容 20 | 21 | 1. `UserAddReactionEvent#getEmoji` 方法在新版本中被移除了,转而增加了 `getReaction` 方法。 22 | 23 | 2. `Guild#getUsers` 的方法签名被修改了,除了 `keyword` 参数,所有其他的是基础数据类型的参数类型均被改为其包装器。(如 `int` -> `Integer`) 24 | 25 | 3. `JKookCommand#register` 方法在新版本中需要 `Plugin` 作为参数,但是旧版本不需要。 26 | 27 | 4. `CommandExecutor`,`UserCommandExecutor`,`ConsoleCommandExecutor` 中 `onCommand` 方法的 `arguments` 参数类型从 `String[]` 变成了 `Object[]` ,关于此变动的详细讲解,见本教程[第 6 章 - 参数解析系统](ch_6/README.md#参数解析系统)。 28 | 29 | 5. 移除了糟糕的 `snw.jkook.event.HandlerList` ,自定义事件不再需要写 `private static final HandlerList handlers = new HandlerList();` 以及 `public static HandlerList getHandlers()` 了。 30 | 31 | 6. `UserClickButtonEvent#getChannel` 方法现在直接返回 `TextChannel` 的实例。 32 | 33 | 34 | ## 如何迁移? 35 | 36 | 对于第 1 条,检查你的事件监听代码中有关原 `getEmoji()` 方法的调用,用 `getReaction().getEmoji()` 替代,即可。 37 | 38 | 因为 Java 有自动装/拆箱的特性,第 2 条在一般情况下可以安全忽略。 39 | * 反射调用的话需要修改传递给 `Class#getMethod` 方法的参数类型的 `Class` 。~~(但为什么要这么做呢?API 0.37 又不是没有这个方法)~~ 40 | 41 | 第 5 条,清理掉所有有关 `HandlerList` 的引用即可。 42 | 43 | 第 6 条,清除 `UserClickButtonEvent` 的监听器中对 `Channel` 的向下转型即可。 44 | 45 | ### 命令部分 46 | 47 | 先检查你的代码,若命令在插件主类中注册,则将所有的 `register()` 替换为 `register(this)` 。 48 | 49 | 反之,将插件实例暴露,然后向 `register` 方法传插件实例的引用即可。 50 | 51 | 然后,有两个方案。 52 | 53 | #### 只做兼容 54 | 55 | 在你完成上文的修改后,若你的 `JKookCommand` 不打算使用新 API 中的参数解析系统,你可以使用以下方案。 56 | 57 | 你可以按如下方法修改你的命令执行器: 58 | 59 | 假设你的原有代码如下: 60 | 61 | ```java 62 | public class Command implements UserCommandExecutor { 63 | 64 | @Override 65 | public void onCommand(CommandSender sender, String[] arguments, @Nullable Message message) { 66 | // command code 67 | } 68 | 69 | } 70 | ``` 71 | 72 | 可以改成: 73 | 74 | ```java 75 | public class Command implements UserCommandExecutor { 76 | 77 | @Override 78 | public void onCommand(CommandSender sender, Object[] arguments, @Nullable Message message) { 79 | onCommand0(sender, toStringArray(arguments), message); 80 | // 也可以使用 81 | // onCommand0(sender, (String[]) arguments, message); 82 | // 但这可能在一些 API 实现中不工作,谁知道呢? 83 | // 前者方法更安全。 84 | } 85 | 86 | private void onCommand0(CommandSender sender, String[] arguments, @Nullable Message message) { 87 | // command code 88 | } 89 | 90 | // 此方法可以提取到一个单独的类,它只是实用方法 91 | public static String[] toStringArray(Object[] array) { 92 | String[] result = new String[array.length]; 93 | int i = 0; 94 | for (Object obj : array) { 95 | result[i++] = obj.toString(); 96 | } 97 | return result; 98 | } 99 | } 100 | ``` 101 | 102 | 为什么可以这么改? 103 | 104 | 在我们设计新的命令系统的时候,我们考虑到了你可能只想简单的兼容高版本。 105 | 106 | 特此,我们在开发新的命令系统的时候,加入了一个特性: 若你从未使用新版本的参数系统,则直接将 `String[]` 作为 `Object[]` 传递给你的命令。 107 | 108 | #### 升级命令 109 | 110 | 既然迁移到了新版本,为什么不用新的命令系统? 111 | 112 | 关于新的参数系统,请见本教程[第 4 章 - 参数解析系统](ch_6/README.md#参数解析系统)。 113 | 114 | 举个例子。 115 | 116 | ```java 117 | new JKookCommand("cmd") 118 | .executesUser(new Command()) 119 | .register(); 120 | ``` 121 | 122 | ```java 123 | public class Command implements UserCommandExecutor { 124 | 125 | @Override 126 | public void onCommand(CommandSender sender, String[] arguments, @Nullable Message message) { 127 | if (arguments.length == 1) { 128 | int a = Integer.parseInt(arguments[0]); 129 | } 130 | } 131 | 132 | } 133 | ``` 134 | 135 | 可以改为: 136 | 137 | ```java 138 | new JKookCommand("cmd") 139 | .addArgument(int.class) 140 | .executeUser(new Command()) 141 | .register(); 142 | ``` 143 | 144 | ```java 145 | public class Command implements UserCommandExecutor { 146 | 147 | @Override 148 | public void onCommand(CommandSender sender, Object[] arguments, @Nullable Message message) { 149 | int a = (int) arguments[0]; 150 | } 151 | 152 | } 153 | ``` 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JKook Tutorial 2 | 3 | 欢迎! 4 | 5 | 此仓库是 [JKook API](https://github.com/SNWCreations/JKook) 的系列教程存放地。 6 | 7 | 通过阅读此仓库的文档,你将了解 JKook API 中的各种类,从而帮助你开发 JKook 插件。 8 | 9 | 若需要从 JKook API 0.37 迁移到最新 API ,见[这篇教程](Migrate_To_Latest.md)。 10 | 11 | ## 观前提示 12 | 13 | 每一章节的文件夹以 `"ch_<章节号>"` 格式命名。 14 | 15 | 具体的示例代码在章节文件夹下的 `code` 文件夹,以 Maven 项目形式呈现。 16 | 17 | 示例代码中会有形如 `/* 1 */` , `/* 2 */` 之类的纯数字 Java 注释,这些数字注释将在章节中的README中以 "第 X 处" 的形式提及。 18 | 19 | 示例代码可以不加修改地编译,并在 JKook API 实现上运行。 20 | 21 | 章节文档中会大量出现诸如 `ClassName#methodName` 的字样,其中 `ClassName` 为一个类的名称,`methodName` 为类中一个方法的名称。 22 | 23 | 章节文档中提及的路径均为以章节目录为根目录的相对路径。 24 | 25 | 若在教程文档中发现你不认识的方法或类,可以查阅 JKook API 的 Javadoc (链接在第 0 章),或者带着疑问继续阅读后续教程(可能有些类在后文会讲)。 26 | 27 | **请确保你已经有 Java 基础,这是编写 JKook 插件的前提。** 28 | * Java 开发教程我们推荐 [廖雪峰](https://www.liaoxuefeng.com/wiki/1252599548343744) 编写的。但其教程始终基于最新版 Java 编写,而我们在 Java 8 ,故可能有些新版本的语法在 Java 8 中不可用。具体的区别请自行辨析。若需要使用更高版本的 Java ,则运行 KookBC 时将需要使用对应版本的 JRE 。更多内容详见 [第 1 章](ch_1/README.md) 的 "配置 KookBC 以用于调试" 部分。 29 | * 你需要掌握:Java 基础语法,Java 基础库(如 `java.util` 包及其中包含的 Java Collection 框架),泛型等 30 | * 学习本教程的内容不需要你刻意学习 Java ME 或 Java EE ,Java SE 就够了。额外的,像 JDBC 数据库操作一类的请自行学习,那也不是本教程的范围。 31 | 32 | 看每一个 Chapter 时,请同时打开其目录下的 README.md 及具体代码,善用你的 IDE 的分栏功能 (如果有) 。 33 | 34 | 推荐使用 IntelliJ IDEA 打开此仓库,可以边读教程边运行示例。 35 | 36 | 也可以在本页面单击空白处,按下键盘上的 `.` 键,即可在 Github.dev 在线编辑器中打开本仓库。**但是仅能读,不能编译运行示例。** 点击 [本链接](https://github.dev/SNWCreations/JKookTutorial) 也可以直接跳转。 37 | 38 | 运行示例请自行准备一个 Bot Token ,如果你不知道怎么获取,[看看这篇教程](Get_Bot_Token.md)。 39 | 40 | ## 环境要求 41 | 42 | **本教程的所有示例在无特殊说明的情况下使用 JKook API 0.48 。** 43 | * 在 `736d39d4` 提交之前,部分章节仍然默认采用 API 0.37 。但是 0.37 版本的长期支持已在 2023-1-31 到期,故我们将所有示例代码迁移了。 44 | 45 | 无特殊说明时,所用工具链如下: 46 | 47 | * OpenJDK 8 48 | * Apache Maven 3.8 49 | 50 | 是的,我们使用 Maven 。Gradle 也十分优秀,但是... 51 | 52 | _作者不会用。_ 53 | 54 | 不过你可以看看 [JKook 插件 Gradle 模板](https://github.com/RealSeek/JKookExample) ,我想只需要把示例的具体代码移动一下...应该就行了吧。 55 | 56 | JDK 与 Maven 的安装这里不作详细讲解。 57 | 58 | KookBC 的使用方法可以在[其仓库](https://github.com/SNWCreations/KookBC)的 README 找到。 59 | 60 | ## 目录 61 | 62 | [Chapter 0 - JKook API 介绍](ch_0/README.md) 63 | 64 | [Chapter 1 - Hello World!](ch_1/README.md) 65 | 66 | [Chapter 2 - 配置文件](ch_2/README.md) 67 | 68 | [Chapter 3 - 再看 Plugin](ch_3/README.md) 69 | 70 | [Chapter 4 - 实体体系概述](ch_4/README.md) 71 | 72 | [Chapter 5 - 消息体系概述](ch_5/README.md) 73 | 74 | [Chapter 6 - 命令系统概述](ch_6/README.md) 75 | 76 | [Chapter 7 - 事件体系概述](ch_7/README.md) 77 | 78 | [Chapter 8 - 任务调度系统](ch_8/README.md) 79 | 80 | [Chapter 9 - 进阶内容](ch_9/README.md) 81 | 82 | [Chapter 10 - 终章!动手实践!](ch_10/README.md) 83 | 84 | ## 贡献 85 | 86 | 啊!我很高兴你愿意为此教程做出贡献! 87 | 88 | 对于教程内容的错误,你可以在 Issue 中提出,或者直接 PR 提交你的修改! 89 | 90 | ## 版权 91 | 92 | JKook Tutorial(即本教程,不包含引用自他人的内容)使用 CC BY-SA 4.0 许可协议。 93 | 94 | 访问 http://creativecommons.org/licenses/by-sa/4.0/ 以查看具体条款。 95 | 96 | 为了使本教程行文方便,文章中引用了一些来自他人的内容,已尽量在文章中列出出处,在此表示一并感激。 97 | 98 | 未列出出处的,找到正确的出处后,也可以向我们提交 Pull Request 以补充。 99 | 100 | 若您发现本教程引用的内容来自您,并且不希望本教程使用,请通过诸如电子邮件等方式联系我们,我们在核实后将予以移除。 101 | -------------------------------------------------------------------------------- /ch_0/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 0 2 | 3 | 很高兴你选择了 JKook 框架作为你的开发框架!我相信 JKook 框架不会让你失望! 4 | 5 | 那么,本章没有示例代码,只有对 JKook API 的介绍,以及一些基础知识。 6 | 7 | ## 总论 8 | 9 | JKook 是一个面向 Java 平台的,对 KOOK API 进行了封装的 API 项目。 10 | 11 | 它以插件为程序的基本单位,多个插件在同一个机器人上同时运行,从而提供不同的功能。 12 | 13 | 它是以 Bukkit API 为原型设计的。因此,若你曾开发过 Bukkit 插件,JKook 对你会很友好。 14 | 15 | 没学过 Bukkit ?没关系,本教程也不需要你刻意学过 Bukkit 。放轻松。 16 | 17 | ## 有用的信息 18 | 19 | [KOOK 官方开发者服务器](https://kook.top/cwznfo) 20 | 21 | [JKook 开发者服务器](https://kook.top/aecCr6) 22 | 23 | 有问题就来这问问吧! 24 | 25 | [JKook API Javadoc (最新 API)](https://jitpack.io/com/github/SNWCreations/JKook/latest/javadoc) 26 | 27 | ~~[JKook API Javadoc (0.37)](https://jitpack.io/com/github/SNWCreations/JKook/0.37.6/javadoc/)~~ 28 | * API 0.37 已经过时。 29 | 30 | 善用 Javadoc ,在遇到不知道用法的方法时去看看! 31 | 32 | ## JKook 插件开发中常用的类、方法 33 | 34 | 本节不讲解以下提及的类与方法,只希望你能在示例代码中看见它们时知道它们是什么,本教程会有专门的章节详细讲解它们。 35 | 36 | 你会在后面的教程的示例代码中经常看见本小节提到的类和方法的。 37 | 38 | `snw.jkook.Core` - 作为所有 JKook API 服务组件的提供者 39 | 40 | `snw.jkook.JKook` - 存放一个 `Core` 的实例,相当于 `Core` 的单例模式封装 41 | * 在 API 0.38+ 下更推荐通过 `snw.jkook.plugin.Plugin#getCore` 方法获取 `Core` 的实例进而调用 42 | 43 | `snw.jkook.plugin.Plugin` - 表示一个插件 44 | 45 | `snw.jkook.plugin.BasePlugin` - `snw.jkook.plugin.Plugin` 接口的一个可用实现,遵循 [JKook 插件格式规范](https://github.com/SNWCreations/JKook/wiki/Plugin-Format) 。推荐插件开发者直接继承此类作为插件的主类。 46 | 47 | `snw.jkook.command.JKookCommand` - 表示一个命令,对此类的详细讲解详见第 6 章 48 | 49 | `snw.jkook.scheduler.Scheduler` - 任务调度器,对此类的详细讲解详见第 8 章 50 | 51 | `snw.jkook.plugin.Plugin#getLogger` - 获取插件的日志记录器 52 | 53 | `snw.jkook.entity.User` - 用户对象,对此类的详细讲解详见第 4 章 54 | 55 | `snw.jkook.entity.Guild` - 服务器对象,对此类的详细讲解详见第 4 章 56 | -------------------------------------------------------------------------------- /ch_1/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 1 2 | 3 | 本章要讲的内容很简单,旨在告诉你一个简单的 JKook 插件如何声明,以及如何配置调试环境。 4 | 5 | 本章是对 [JKook Wiki - 插件格式规范](https://github.com/SNWCreations/JKook/wiki/Plugin-Format) 的更深入讲解,**示例代码有所不同**。 6 | 7 | `code/src/main/resources/plugin.yml` 文件是插件元数据文件,用于描述插件详情。其结构详解可以在 JKook 插件格式规范 Wiki 页面 (链接见上文) 中找到,此处不再赘述。 8 | 9 | 请打开 `code/src/main/java/snw/jkook/example/Main.java` 文件。 10 | 11 | --- 12 | 13 | 从头看起,让我们看第 1 处。 14 | 15 | `extends BasePlugin` 表明 Main 类继承 `BasePlugin` 类,而 `BasePlugin` 是 `Plugin` 接口类的一个可用实现。 16 | 17 | **注意,plugin.yml 中指定的主类必须是 `Plugin` 类 (不绝对是 `BasePlugin` 类,因为其只是 `Plugin` 类的一种实现) 的子类,否则插件无效。** 18 | 19 | --- 20 | 21 | 再看第 2 处。 22 | 23 | onLoad 方法在插件被加载后,启用前调用。 24 | 25 | 通过覆盖此方法即可在启用插件进行一些初始化操作,如释放默认配置。 26 | 27 | **注意,默认配置若在 onEnable 阶段释放将无法被加载!** 28 | 29 | 相信你已经注意到了一些 `getLogger` 的调用。 30 | 31 | 这里顺带讲讲 JKook 的日志框架。 32 | 33 | JKook 框架使用 [SLF4J](https://www.slf4j.org) 框架作为日志框架。 34 | 35 | 没听说过?没关系,这里简单讲讲。 36 | 37 | SLF4J 提供了多种日志级别,以下列出常用的: 38 | 39 | ```text 40 | ERROR - 错误 (影响了程序的正常运行) 41 | WARN - 警告 (不影响程序的正常运行,但是可能会发生一点预期之外的事情) 42 | INFO - 信息 (一般用于程序的正常运行过程中的重要事件) 43 | DEBUG - 调试 (一般用于记录程序的正常运行过程中的细节) 44 | ``` 45 | 46 | 严重程度依次递减。 47 | 48 | `Plugin#getLogger` 方法返回 `org.slf4j.Logger` 的实例,即日志记录器。 49 | 50 | 使用以上日志级别记录日志,调用 `Logger` 中的对应方法即可。 51 | 52 | 日志方法的命名规范是全小写,如 `Logger#error` 方法对应 `ERROR` 等级。 53 | 54 | --- 55 | 56 | 现在,转到第 3 处。 57 | 58 | 若 onEnable 方法被调用,则意味着你的插件被启用了,在这个阶段,可以进行诸如加载配置,启动你的服务等操作。 59 | 60 | **注意,若你的代码在以上两个阶段抛出异常,则插件将被禁用。** 61 | 62 | 但是,当你的插件真的出现了无法处理的异常,让你的插件被禁用无疑是最安全的。 63 | 64 | --- 65 | 66 | 接着看第 4 处。 67 | 68 | 当 onDisable 方法被调用时,意味着你该关闭你的服务了。 69 | 70 | 在这个阶段,请关闭你的所有服务,清理不再使用的数据,保存需要持久化存储的数据。 71 | 72 | **注意: onDisable 被调用并不意味着你的插件不会再被启用。有可能在一个 JVM 中,你的插件会被启用/禁用多次。_对插件的管理是由 `PluginManager` 接口实现的。_** 73 | 74 | --- 75 | 76 | 这里讲一个小技巧,我们来看第 5 处。 77 | 78 | 这行代码将 `this` (当前插件的实例) 赋值到一个 `static` 变量 `instance` ,然后就可以通过第 6 处的方法获得插件的实例。 79 | 80 | **为什么这么做?** 81 | 82 | 在后面的教程中,你会发现有些方法需要 `Plugin` 的实例,而这时就需要一个方法将你的插件实例传递给这些方法。 83 | 84 | 通过 `public static` 方法在哪都能调用的特性,我们可以轻松解决这个问题。 85 | 86 | 也建议你在编写新的 JKook 插件时用上这个小技巧,你的插件开发会更方便。 87 | 88 | **我不能 `new` 一个插件传进去吗?** 89 | 90 | 别那样做。插件应该遵循单例模式设计(即从始到终,同一个类型的对象只应该存在一个),对于同一个插件,存在多个实例会引起混乱。并且,通过 `new` 得到的插件实例未经过初始化,不能正常使用。 91 | 92 | ## 配置 KookBC 以用于调试 93 | 94 | KookBC 是一个主要由 JKook API 作者 ZX夏夜之风 开发并维护的 JKook API 实现。 95 | 96 | KookBC 提供了完整的 JKook API 功能实现。 97 | 98 | 请自行准备 Java 运行时环境(JRE,Java Runtime Environment)。一般情况下使用 Java 8 即可。 99 | 100 | 若插件使用了更高版本的 Java API ,则请准备插件所用的 Java 所对应的运行时环境。 101 | 102 | 若多个插件均使用了不同的 Java 版本,取其中的最高版本作为使用的 Java 运行时环境的版本。 103 | 104 | 对于开发者,请安装 JDK ,而不是仅安装 JRE 。JDK 中包含 JRE 。 105 | 106 | Java 运行时环境可以在以下地址下载: 107 | * [清华大学开源软件镜像站 - Eclipse Adoptium OpenJDK](https://mirror.tuna.tsinghua.edu.cn/Adoptium) 108 | 109 | 具体的安装过程此处不再赘述。 110 | 111 | 请打开 [KookBC Releases](https://github.com/SNWCreations/KookBC/releases) 页面,下载你需要的 KookBC 版本。 112 | 113 | JKook API 0.48.x 对应 KookBC 0.25.x 及以上版本。 114 | 115 | ~~JKook API 0.37.x 对应 KookBC 0.17.x 。~~ 116 | * **API 0.37 已经过时。** 推荐使用最新版本 API ,后面的教程也都采用最新版本。 117 | 118 | 下载对应版本的最新版本,以保证得到最新的漏洞修补。 119 | 120 | 下载好后,在一个你认为合适的地方新建文件夹(路径尽量纯英文),将 KookBC 放入。 121 | 122 | 接下来的部分可以参照 [KookBC 仓库主页](https://github.com/SNWCreations/KookBC) 的 README 操作。 123 | 124 | ### 使用 IntelliJ IDEA 调试你的插件 125 | 126 | 以下内容基于 IntelliJ IDEA 2022.1.4 版。 127 | 128 | 既然都有现代化的 IDE 用于开发了,为什么不用现代化的调试方法呢? 129 | 130 | 毕竟,工具用得好,事半功倍。 131 | 132 | 在开始以下内容前,请先准备一份 KookBC ,一个 KOOK 机器人 Token (不知道怎么获取? 读[这份教程](../Get_Bot_Token.md))。 133 | 134 | 首先,用 IDEA 打开你的插件项目,找到图中的 "添加配置" 并点击。 135 | 136 | ![](images/0.png) 137 | 138 | 若你的项目已经有至少 1 个运行配置了,请看下图,找到图中的 "修改配置" 并点击。 139 | 140 | ![](images/1.png) 141 | 142 | 新建一个类型为 "JAR 应用程序" 的运行配置。 143 | 144 | ![](images/2.png) 145 | 146 | 配置 JAR 路径为 KookBC JAR 。 147 | 148 | ![](images/3.png) 149 | 150 | ![](images/4.png) 151 | 152 | 然后,配置 "程序实参" 和 "工作目录"。 153 | 154 | 工作目录可以随意,但这会影响你的 KookBC 数据所在位置 (如 plugins 文件夹和 kbc.yml 等数据会在工作目录创建),一般情况下直接设置为 KookBC JAR 所在位置即可。 155 | 156 | 若你未配置 kbc.yml ,则可以在程序实参中通过 `--token` 命令行参数向 KookBC 传递你的 Token 。 157 | 158 | 更多的 KookBC 设置仍然需要看 kbc.yml 。 159 | 160 | ![](images/5.png) 161 | 162 | 在 KookBC 完成配置后,以下内容就是你一般调试的流程了。 163 | 164 | 编译你的插件,将其放到调试用的 KookBC 的 plugins 文件夹。 165 | 166 | 然后在你的插件源代码中打上断点,接着以调试模式启动你的 KookBC 运行配置。 167 | 168 | 断点应该会正常工作。 169 | 170 | ![](images/6.png) 171 | 172 | ![](images/7.png) 173 | 174 | --- 175 | 176 | 至此,本章内容结束了。 177 | 178 | 本章我们了解了 JKook 插件的声明,以及调试环境的配置。 179 | 180 | 如果你准备好了,进入下一章吧!该聊聊命令系统了。 181 | -------------------------------------------------------------------------------- /ch_1/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_1/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_1/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.plugin.BasePlugin; 4 | 5 | public class Main extends BasePlugin /* 1 */ { 6 | private static Main instance; 7 | 8 | @Override 9 | public void onLoad() /* 2 */ { 10 | instance = this; /* 5 */ 11 | getLogger().info("Hello world plugin loaded!"); 12 | } 13 | 14 | @Override 15 | public void onEnable() /* 3 */ { 16 | getLogger().info("Hello world plugin enabled!"); 17 | } 18 | 19 | @Override 20 | public void onDisable() /* 4 */ { 21 | getLogger().info("Hello world plugin disabled!"); 22 | } 23 | 24 | /* 6 */ 25 | public static Main getInstance() { 26 | return instance; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ch_1/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_1/images/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/0.png -------------------------------------------------------------------------------- /ch_1/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/1.png -------------------------------------------------------------------------------- /ch_1/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/2.png -------------------------------------------------------------------------------- /ch_1/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/3.png -------------------------------------------------------------------------------- /ch_1/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/4.png -------------------------------------------------------------------------------- /ch_1/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/5.png -------------------------------------------------------------------------------- /ch_1/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/6.png -------------------------------------------------------------------------------- /ch_1/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_1/images/7.png -------------------------------------------------------------------------------- /ch_10/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 10 2 | 3 | **本章示例代码不需要你在读下文前运行。** 4 | 5 | **相反,在读完本章并完成本章交给你的任务之前请不要看示例代码。** 6 | 7 | --- 8 | 9 | 你读到了这一章。 10 | 11 | 本章没有新理论了。 12 | 13 | 在这里,我会给出一个需求,你可以使用自己已经学到的 JKook 知识编写一个符合以下需求的插件。 14 | 15 | 这个需求如下: 16 | * 实现一个简单的积分系统。 17 | * 当用户在机器人可以访问的频道中发出一条消息时,用户的分数+1。 18 | * 用户可以通过积累积分升级,等级越高,升到下一级所需要的分数就越高。 19 | * 提供一个命令,可以用于查询当前服务器积分前 10 的用户。 20 | * 提供一个命令,使用户可以查询自己的分数和自己在当前服务器中的排名。 21 | * 提供一个命令,使有消息管理权限的用户可以修改用户的分数。 22 | * 当用户加入服务器时,向 TA 发送一条私信,内容随意。 23 | * 当用户离开服务器时,清除 TA 的分数。 24 | * 每 10--15 分钟(具体时间可以用随机数决定,也可以写死),发送一个随机生成的数学算式(仅限于四则运算),最先回答出答案的用户的分数+10,若 1 分钟内无人回答,则公布答案,同时本次问答作废。 25 | * 应该提供一个命令用于指定发布算式的频道,且用户的答案只有在指定的频道发出才有效。这个命令需要用户有消息管理权限。 26 | 27 | 试试实现它? 28 | 29 | --- 30 | 31 | 在自己实现后再去看示例代码吧。 32 | 33 | 学会了知识,总要动手实践的。 34 | 35 | 至此,JKook Tutorial 完结。 36 | 37 | 欢迎加入 [JKook 开发者社区](https://kook.top/aecCr6) 和我们探讨你学习 JKook 框架的收获! 38 | 39 | 感谢你的阅读。 40 | 41 | 这不是学习的结束,而是刚刚开始。 42 | -------------------------------------------------------------------------------- /ch_10/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_10/code/README.md: -------------------------------------------------------------------------------- 1 | # The Final Project 2 | 3 | 哦,你发现了这个 README 文件。 4 | 5 | 这是由我自己编写的,基于我对第 10 章的要求的理解编写的插件。 6 | 7 | 这个插件提供如下命令: 8 | * /setscore @某人 分数 - 修改某人的分数 9 | * /rank - 查询自己的分数 10 | * /ranklist - 获取服务器的积分榜。仅展示前 10 个。 11 | * /active - 将服务器的活跃频道设为发出命令时的消息所在的频道。活跃频道将被用于发送随机算式。 12 | 13 | 这个示例我使用了 2 天编写。 14 | 15 | 因为仅仅是为了教学用途,所以未考虑向多个服务器服务时的性能,所以如果你想直接部署它然后就提供服务的话,那还是算了吧... 16 | 17 | 我一度在编写求解随机算式的实现时遇到了困难,反复重写了多个版本的算法,但都不好用。 18 | 19 | 所以最终我使用了 Nashorn JavaScript Engine 完成求解随机算式的过程。lol 20 | 21 | 你已经自己完成了示例?看看我的代码吧!也许你还可以从中学到什么。 22 | 23 | 就这样吧! 24 | 25 | --- SNWCreations, 2023/1/22 26 | -------------------------------------------------------------------------------- /ch_10/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | ${maven.compiler.target} 15 | 16 | 17 | 18 | 19 | jitpack.io 20 | https://jitpack.io 21 | 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-compiler-plugin 29 | 3.8.1 30 | 31 | ${java.version} 32 | ${java.version} 33 | -Xlint:unchecked 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-jar-plugin 39 | 3.2.0 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-shade-plugin 44 | 3.3.0 45 | 46 | 47 | shade 48 | package 49 | 50 | shade 51 | 52 | 53 | false 54 | 55 | 56 | *:* 57 | 58 | META-INF/*.SF 59 | META-INF/*.DSA 60 | META-INF/*.RSA 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | src/main/resources 72 | true 73 | 74 | 75 | 76 | 77 | 78 | 79 | com.github.SNWCreations 80 | JKook 81 | 0.49.0 82 | provided 83 | 84 | 85 | 89 | 90 | org.openjdk.nashorn 91 | nashorn-core 92 | 15.4 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/EventListeners.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.entity.channel.TextChannel; 4 | import snw.jkook.event.EventHandler; 5 | import snw.jkook.event.Listener; 6 | import snw.jkook.event.channel.ChannelMessageEvent; 7 | import snw.jkook.event.role.RoleInfoUpdateEvent; 8 | import snw.jkook.event.user.UserJoinGuildEvent; 9 | import snw.jkook.event.user.UserLeaveGuildEvent; 10 | import snw.jkook.example.events.LevelUpEvent; 11 | import snw.jkook.message.component.MarkdownComponent; 12 | 13 | public class EventListeners implements Listener { 14 | 15 | @EventHandler 16 | public void onJoin(UserJoinGuildEvent event) { 17 | event.getUser().sendPrivateMessage(new MarkdownComponent("欢迎!")); 18 | } 19 | 20 | @EventHandler 21 | public void onLeave(UserLeaveGuildEvent event) { 22 | ScoreboardStorage.getScoreboard(event.getGuild()).reset(event.getUser()); 23 | } 24 | 25 | @EventHandler 26 | public void onMessage(ChannelMessageEvent event) { 27 | if (event.getMessage().getSender() == Main.getInstance().getCore().getUser()) { 28 | return; // do not record self 29 | } 30 | ScoreboardStorage.getScoreboard(event.getChannel().getGuild()) 31 | .calc(event.getMessage().getSender(), i -> i + 1); 32 | } 33 | 34 | @EventHandler 35 | public void onRoleUpdate(RoleInfoUpdateEvent event) { 36 | PermissionStorage.update(event.getRole()); 37 | } 38 | 39 | // The custom event! 40 | @EventHandler 41 | public void onLevelUp(LevelUpEvent event) { 42 | TextChannel textChannel = GuildChannelStorage.get(event.getGuild()); 43 | if (textChannel == null) { 44 | return; 45 | } 46 | textChannel.sendComponent( 47 | new MarkdownComponent( 48 | "恭喜 " + "(met)" + event.getUser().getId() + "(met)" + " 升至 " + event.getNow() + " 级!" 49 | ) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/GuildChannelStorage.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.config.ConfigurationSection; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.channel.TextChannel; 6 | import snw.jkook.example.math.MathEventPublisher; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | // We should save this 12 | public class GuildChannelStorage { 13 | private static final Map map = new ConcurrentHashMap<>(); // key: guild id, value: channel id 14 | 15 | public static TextChannel get(Guild guild) { 16 | String id = map.get(guild.getId()); 17 | if (id == null) { 18 | return null; 19 | } 20 | try { 21 | return (TextChannel) Main.getInstance().getCore().getHttpAPI().getChannel(id); 22 | } catch (ClassCastException e) { 23 | map.remove(guild.getId()); // invalid channel type! 24 | return null; 25 | } 26 | } 27 | 28 | public static void set(Guild guild, TextChannel channel) { 29 | map.put(guild.getId(), channel.getId()); 30 | new MathEventPublisher(guild).start(); 31 | } 32 | 33 | public static void loadAll(ConfigurationSection section) { 34 | Map values = section.getValues(false); 35 | for (Map.Entry entry : values.entrySet()) { 36 | try { 37 | map.put(entry.getKey(), (String) entry.getValue()); 38 | new MathEventPublisher(Main.getInstance().getCore().getHttpAPI().getGuild(entry.getKey())).start(); 39 | } catch (Exception ignored) {} 40 | } 41 | } 42 | 43 | public static void saveAll(ConfigurationSection section) { 44 | for (Map.Entry entry : map.entrySet()) { 45 | section.set(entry.getKey(), entry.getValue()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.command.JKookCommand; 4 | import snw.jkook.config.file.YamlConfiguration; 5 | import snw.jkook.config.serialization.ConfigurationSerialization; 6 | import snw.jkook.entity.User; 7 | import snw.jkook.example.commands.ActiveCommand; 8 | import snw.jkook.example.commands.RankCommand; 9 | import snw.jkook.example.commands.RankListCommand; 10 | import snw.jkook.example.commands.SetScoreCommand; 11 | import snw.jkook.plugin.BasePlugin; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | 16 | public class Main extends BasePlugin { 17 | private static Main instance; 18 | 19 | @Override 20 | public void onLoad() { 21 | instance = this; 22 | ConfigurationSerialization.registerClass(Scoreboard.class); 23 | } 24 | 25 | @Override 26 | public void onEnable() { 27 | loadConfigs(); 28 | registerCommands(); 29 | getCore().getEventManager().registerHandlers(this, new EventListeners()); 30 | } 31 | 32 | @Override 33 | public void onDisable() { 34 | saveConfigs(); 35 | } 36 | 37 | private void loadConfigs() { 38 | try { 39 | YamlConfiguration data = YamlConfiguration.loadConfiguration(new File(getDataFolder(), "data.yml")); 40 | ScoreboardStorage.loadAll(data); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | try { 45 | YamlConfiguration gcc = YamlConfiguration.loadConfiguration(new File(getDataFolder(), "guildchannelmapping.yml")); 46 | GuildChannelStorage.loadAll(gcc); 47 | } catch (Exception e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | 52 | private void saveConfigs() { 53 | YamlConfiguration score = new YamlConfiguration(); 54 | ScoreboardStorage.saveAll(score); 55 | try { 56 | score.save(new File(getDataFolder(), "data.yml")); 57 | } catch (IOException e) { 58 | // you should record the data in other way if the data is important. e.g. print it to logger? 59 | getLogger().error("Unable to save data, we will print it here"); 60 | getLogger().error(score.saveToString()); 61 | } 62 | 63 | YamlConfiguration gcc = new YamlConfiguration(); 64 | GuildChannelStorage.saveAll(gcc); 65 | try { 66 | gcc.save(new File(getDataFolder(), "guildchannelmapping.yml")); 67 | } catch (IOException e) { 68 | // you should record the data in other way if the data is important. e.g. print it to logger? 69 | getLogger().error("Unable to save guild->channel mapping, we will print it here"); 70 | getLogger().error(gcc.saveToString()); 71 | } 72 | } 73 | 74 | private void registerCommands() { 75 | new JKookCommand("rank") 76 | .setDescription("使用此命令获取你在此服务器中的积分数据!") 77 | .executesUser(new RankCommand()) 78 | .register(this); 79 | new JKookCommand("ranklist") 80 | .setDescription("使用此命令获取此服务器的积分榜。仅展示前 10 个。") 81 | .executesUser(new RankListCommand()) 82 | .register(this); 83 | new JKookCommand("setscore") 84 | .setDescription("使用此命令设置指定用户的分数。需要你有消息管理权限。使用 /help setscore 获取详细帮助。") 85 | .setHelpContent("/setscore @某人 分数") 86 | .addArgument(User.class) 87 | .addArgument(int.class) 88 | .executesUser(new SetScoreCommand()) 89 | .register(this); 90 | new JKookCommand("active") 91 | .setDescription("将当前服务器的活跃频道设为发出命令时的消息所在的频道。活跃频道将被用于发送随机算式。需要你有消息管理权限。") 92 | .executesUser(new ActiveCommand()) 93 | .register(this); 94 | } 95 | 96 | public static Main getInstance() { 97 | return instance; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/PermissionStorage.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.Permission; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.Role; 6 | import snw.jkook.entity.User; 7 | import snw.jkook.util.PageIterator; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | // We shouldn't save the cache 16 | public class PermissionStorage { 17 | private static final Map> map = new ConcurrentHashMap<>(); 18 | 19 | public static boolean hasPermission(User user, Guild guild) { 20 | Collection roles = user.getRoles(guild); 21 | return get(guild).stream().anyMatch(roles::contains); 22 | } 23 | 24 | public static Collection get(Guild guild) { 25 | Collection result; 26 | result = map.get(guild.getId()); 27 | if (result != null) { 28 | return result; 29 | } 30 | result = new ArrayList<>(); 31 | PageIterator> iter = guild.getRoles(); 32 | while (iter.hasNext()) { 33 | Set next = iter.next(); 34 | for (Role role : next) { 35 | if (role.isPermissionSet(Permission.MESSAGE_MANAGE)) { 36 | result.add(role.getId()); 37 | } 38 | } 39 | } 40 | map.put(guild.getId(), result); 41 | return result; 42 | } 43 | 44 | public static void update(Role role) { 45 | Collection c = map.computeIfAbsent(role.getGuild().getId(), i -> new ArrayList<>()); 46 | if (role.isPermissionSet(Permission.MESSAGE_MANAGE)) { 47 | c.add(role.getId()); 48 | } else { 49 | c.remove(role.getId()); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/Scoreboard.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.config.serialization.ConfigurationSerializable; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.User; 6 | import snw.jkook.example.events.LevelUpEvent; 7 | 8 | import java.util.*; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.function.DoubleFunction; 11 | import java.util.function.IntFunction; 12 | 13 | // Represents a scoreboard. 14 | // Map keys are all user ID. 15 | public class Scoreboard implements ConfigurationSerializable { 16 | private static final int FIRST_LEVEL = 100; 17 | private static final DoubleFunction TO_NEXT_LEVEL = (i) -> (i * 1.5); 18 | 19 | private final Guild guild; 20 | private final Map map; 21 | private final Map levelMap; 22 | 23 | public Scoreboard(Guild guild) { 24 | this(guild, new ConcurrentHashMap<>()); 25 | } 26 | 27 | public Scoreboard(String guildId, Map map) { 28 | this(Main.getInstance().getCore().getHttpAPI().getGuild(guildId), map); 29 | } 30 | 31 | public Scoreboard(Guild guild, Map map) { 32 | this.guild = guild; 33 | this.map = map; 34 | this.levelMap = calcLevelMap(map); // restore level data 35 | } 36 | 37 | public void calc(User user, IntFunction calcLogic) { 38 | int previous = map.computeIfAbsent(user.getId(), i -> 0); // NotNull, so int 39 | Integer result = calcLogic.apply(previous); 40 | if (result == null) { 41 | throw new NullPointerException("result cannot be null"); 42 | } 43 | setScore(user, result); 44 | } 45 | 46 | public int getScore(User user) { 47 | return map.computeIfAbsent(user.getId(), i -> 0); 48 | } 49 | 50 | public int getLevel(User user) { 51 | return levelMap.computeIfAbsent(user.getId(), i -> (Integer) scoreToLevel(getScore(user))[0]); 52 | } 53 | 54 | public List> getSortedLevelEntries() { 55 | List> entries = new ArrayList<>(levelMap.entrySet()); 56 | entries.sort((o1, o2) -> { 57 | int a = o1.getValue(); 58 | int b = o2.getValue(); 59 | return Integer.compare(b, a); 60 | }); 61 | return entries; 62 | } 63 | 64 | public void setScore(User target, int newScore) { 65 | updateLevel(target, newScore); 66 | map.put(target.getId(), newScore); 67 | } 68 | 69 | public void reset(User user) { 70 | map.remove(user.getId()); 71 | } 72 | 73 | private void updateLevel(User user, int score) { 74 | int prevLevel = getLevel(user); 75 | int newLevel = (int) scoreToLevel(score)[0]; 76 | if (newLevel > prevLevel) { 77 | Main.getInstance().getCore().getEventManager().callEvent(new LevelUpEvent(guild, user, prevLevel, newLevel)); 78 | levelMap.put(user.getId(), newLevel); 79 | } 80 | } 81 | 82 | // The methods that related to serialization: 83 | 84 | @Override 85 | public Map serialize() { 86 | Map result = new HashMap<>(); 87 | result.put("guild_id", guild.getId()); 88 | result.put("data", map); 89 | return result; 90 | } 91 | 92 | @SuppressWarnings("unused") // used by configuration serialization system 93 | public static ConfigurationSerializable deserialize(Map data) { 94 | //noinspection unchecked 95 | return new Scoreboard((String) data.get("guild_id"), new ConcurrentHashMap<>((Map) data.get("data"))); 96 | } 97 | 98 | // Utility methods: 99 | 100 | // format: 101 | // [0] -> current level (int) 102 | // [1] -> the score needed to next level (double) 103 | // [2] -> the point of the next level (double) 104 | public static Number[] scoreToLevel(int score) { 105 | if (score < FIRST_LEVEL) { 106 | return new Number[]{0, FIRST_LEVEL - score, FIRST_LEVEL}; 107 | } 108 | int level = 1; 109 | double score1 = score - FIRST_LEVEL; 110 | double n = FIRST_LEVEL; 111 | double t; // temp 112 | while ((t = (score1 - (n = TO_NEXT_LEVEL.apply(n)))) >= 0) { 113 | score1 = t; 114 | level++; 115 | } 116 | return new Number[]{level, score1, n}; 117 | } 118 | 119 | private static Map calcLevelMap(Map data) { 120 | Map result = new HashMap<>(); 121 | for (Map.Entry entry : data.entrySet()) { 122 | int i = (int) scoreToLevel(entry.getValue())[0]; 123 | result.put(entry.getKey(), i); 124 | } 125 | return result; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/ScoreboardStorage.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.config.ConfigurationSection; 4 | import snw.jkook.entity.Guild; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | public class ScoreboardStorage { 10 | private static final Map storage; 11 | 12 | static { 13 | storage = new ConcurrentHashMap<>(); 14 | } 15 | 16 | private ScoreboardStorage() {} // just a util class 17 | 18 | public static Scoreboard getScoreboard(Guild guild) { 19 | return storage.computeIfAbsent(guild.getId(), i -> new Scoreboard(guild)); 20 | } 21 | 22 | public static void loadAll(ConfigurationSection section) { 23 | Map values = section.getValues(false); 24 | for (Map.Entry entry : values.entrySet()) { 25 | try { 26 | storage.put(entry.getKey(), (Scoreboard) entry.getValue()); 27 | } catch (Exception ignored) {} 28 | } 29 | } 30 | 31 | public static void saveAll(ConfigurationSection section) { 32 | for (Map.Entry entry : storage.entrySet()) { 33 | section.set(entry.getKey(), entry.getValue()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/commands/ActiveCommand.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.commands; 2 | 3 | import snw.jkook.command.UserCommandExecutor; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.User; 6 | import snw.jkook.entity.channel.TextChannel; 7 | import snw.jkook.example.GuildChannelStorage; 8 | import snw.jkook.example.PermissionStorage; 9 | import snw.jkook.message.Message; 10 | import snw.jkook.message.TextChannelMessage; 11 | import snw.jkook.message.component.MarkdownComponent; 12 | 13 | public class ActiveCommand implements UserCommandExecutor { 14 | @Override 15 | public void onCommand(User sender, Object[] arguments, Message message) { 16 | if (message instanceof TextChannelMessage) { 17 | TextChannel channel = ((TextChannelMessage) message).getChannel(); 18 | Guild guild = channel.getGuild(); 19 | if (PermissionStorage.hasPermission(sender, guild)) { 20 | GuildChannelStorage.set(guild, channel); 21 | message.reply(new MarkdownComponent("操作成功。")); 22 | } else { 23 | message.reply(new MarkdownComponent("无权操作。")); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/commands/RankCommand.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.commands; 2 | 3 | import snw.jkook.command.UserCommandExecutor; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.User; 6 | import snw.jkook.example.Scoreboard; 7 | import snw.jkook.example.ScoreboardStorage; 8 | import snw.jkook.message.Message; 9 | import snw.jkook.message.TextChannelMessage; 10 | 11 | public class RankCommand implements UserCommandExecutor { 12 | @Override 13 | public void onCommand(User sender, Object[] arguments, Message message) { 14 | if (message instanceof TextChannelMessage) { 15 | TextChannelMessage txtmsg = (TextChannelMessage) message; 16 | Guild guild = txtmsg.getChannel().getGuild(); 17 | Scoreboard scoreboard = ScoreboardStorage.getScoreboard(guild); 18 | StringBuilder contentBuilder = new StringBuilder(); 19 | Number[] levelData = Scoreboard.scoreToLevel(scoreboard.getScore(sender)); 20 | contentBuilder.append(sender.getNickName(guild)).append("#").append(sender.getIdentifyNumber()).append("\n"); 21 | contentBuilder.append("等级: ").append(levelData[0]).append("\n"); 22 | contentBuilder.append("积分: ").append(levelData[1]).append("/").append(levelData[2]); 23 | message.reply(contentBuilder.toString()); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/commands/RankListCommand.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.commands; 2 | 3 | import snw.jkook.command.UserCommandExecutor; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.User; 6 | import snw.jkook.example.Main; 7 | import snw.jkook.example.Scoreboard; 8 | import snw.jkook.example.ScoreboardStorage; 9 | import snw.jkook.message.Message; 10 | import snw.jkook.message.TextChannelMessage; 11 | import snw.jkook.message.component.MarkdownComponent; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class RankListCommand implements UserCommandExecutor { 17 | @Override 18 | public void onCommand(User sender, Object[] arguments, Message message) { 19 | if (message instanceof TextChannelMessage) { 20 | TextChannelMessage txtmsg = (TextChannelMessage) message; 21 | Guild guild = txtmsg.getChannel().getGuild(); 22 | Scoreboard scoreboard = ScoreboardStorage.getScoreboard(guild); 23 | List> entries = scoreboard.getSortedLevelEntries(); 24 | if (entries.isEmpty()) { 25 | message.reply(new MarkdownComponent("积分榜为空。")); 26 | return; 27 | } 28 | 29 | StringBuilder contentBuilder = new StringBuilder(); 30 | contentBuilder.append("========== 积分榜 ==========").append("\n"); 31 | for (int i = 0; i < 10; i++) { 32 | if (entries.size() < (i + 1)) { 33 | break; // not enough data! 34 | } 35 | Map.Entry entry = entries.get(i); 36 | User user = Main.getInstance().getCore().getHttpAPI().getUser(entry.getKey()); 37 | Number[] levelData = Scoreboard.scoreToLevel(scoreboard.getScore(user)); 38 | contentBuilder.append("#").append(i).append(" - ").append(user.getNickName(guild)).append("#").append(user.getIdentifyNumber()).append("\n"); 39 | contentBuilder.append("等级: ").append(levelData[0]).append("\n"); 40 | contentBuilder.append("积分: ") 41 | .append(levelData[2].intValue() - levelData[1].intValue()) 42 | .append("/") 43 | .append(levelData[2]).append("\n"); 44 | if (entries.size() > (i + 1)) { // if not the last round, and there will have next round 45 | contentBuilder.append("---\n"); // append new line separator 46 | } 47 | } 48 | message.reply(contentBuilder.toString()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/commands/SetScoreCommand.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.commands; 2 | 3 | import snw.jkook.command.UserCommandExecutor; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.User; 6 | import snw.jkook.example.PermissionStorage; 7 | import snw.jkook.example.Scoreboard; 8 | import snw.jkook.example.ScoreboardStorage; 9 | import snw.jkook.message.Message; 10 | import snw.jkook.message.TextChannelMessage; 11 | import snw.jkook.message.component.MarkdownComponent; 12 | 13 | public class SetScoreCommand implements UserCommandExecutor { 14 | 15 | // expected: 16 | // arguments[0] = User 17 | // arguments[1] = int 18 | @Override 19 | public void onCommand(User sender, Object[] arguments, Message message) { 20 | if (message instanceof TextChannelMessage) { 21 | TextChannelMessage txtmsg = (TextChannelMessage) message; 22 | User target = (User) arguments[0]; 23 | int newScore = (int) arguments[1]; 24 | Guild guild = txtmsg.getChannel().getGuild(); 25 | if (PermissionStorage.hasPermission(sender, guild)) { 26 | Scoreboard scoreboard = ScoreboardStorage.getScoreboard(guild); 27 | scoreboard.setScore(target, newScore); 28 | message.reply(new MarkdownComponent("操作成功。")); 29 | } else { 30 | message.reply(new MarkdownComponent("无权操作。")); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/events/LevelUpEvent.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.events; 2 | 3 | import snw.jkook.entity.Guild; 4 | import snw.jkook.entity.User; 5 | import snw.jkook.event.Event; 6 | 7 | public class LevelUpEvent extends Event { 8 | private final Guild guild; 9 | private final User user; 10 | private final int prev; 11 | private final int now; 12 | 13 | public LevelUpEvent(Guild guild, User user, int prev, int now) { 14 | this.guild = guild; 15 | this.user = user; 16 | this.prev = prev; 17 | this.now = now; 18 | } 19 | 20 | public Guild getGuild() { 21 | return guild; 22 | } 23 | 24 | public User getUser() { 25 | return user; 26 | } 27 | 28 | public int getPrev() { 29 | return prev; 30 | } 31 | 32 | public int getNow() { 33 | return now; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/math/MathEventContainer.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.math; 2 | 3 | public class MathEventContainer { 4 | public static RandomMath question; 5 | } 6 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/math/MathEventPublisher.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.math; 2 | 3 | import snw.jkook.entity.Guild; 4 | import snw.jkook.event.EventHandler; 5 | import snw.jkook.event.Listener; 6 | import snw.jkook.event.channel.ChannelMessageEvent; 7 | import snw.jkook.example.GuildChannelStorage; 8 | import snw.jkook.example.Main; 9 | import snw.jkook.example.ScoreboardStorage; 10 | import snw.jkook.message.component.MarkdownComponent; 11 | import snw.jkook.scheduler.JKookRunnable; 12 | import snw.jkook.scheduler.Task; 13 | 14 | import java.util.Objects; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | public class MathEventPublisher extends JKookRunnable implements Listener { 18 | private final Guild guild; 19 | 20 | public MathEventPublisher(Guild guild) { 21 | this.guild = guild; 22 | } 23 | 24 | @Override 25 | public void run() { 26 | MathEventContainer.question = new RandomMath(); 27 | String msgid = Objects.requireNonNull(GuildChannelStorage.get(guild)).sendComponent( 28 | "答题时间!" + 29 | // replace * using \* to disable Markdown syntax 30 | MathEventContainer.question.getCalcString().replace("*", "\\*") 31 | + "=?" + "\n" + 32 | "抢先答对的奖励 10 积分!" 33 | ); 34 | // Delete the question if there is nobody answered it, if there is too many question message, it is a spam! 35 | Task delQuestionTask = new JKookRunnable() { 36 | @Override 37 | public void run() { 38 | try { 39 | // Delete message 40 | Main.getInstance().getCore().getUnsafe().getTextChannelMessage(msgid).delete(); 41 | } catch (Exception e) { 42 | Main.getInstance().getLogger().warn("Unable to delete question", e); 43 | } 44 | } 45 | }.runTaskLater(Main.getInstance(), TimeUnit.MINUTES.toMillis(1)); 46 | // The core logic of user input 47 | Listener l = new Listener() { 48 | @EventHandler 49 | public void onMessage(ChannelMessageEvent event) { 50 | if (event.getChannel() == GuildChannelStorage.get(guild)) { 51 | if (event.getMessage().getComponent() instanceof MarkdownComponent) { 52 | if (MathEventContainer.question == null) return; // no question now 53 | if (Objects.equals(MathEventContainer.question.getResult(), event.getMessage().getComponent().toString())) { 54 | ScoreboardStorage.getScoreboard(guild).calc(event.getMessage().getSender(), i -> i + 10); 55 | MathEventContainer.question = null; 56 | if (!delQuestionTask.isCancelled() && !delQuestionTask.isExecuted()) { 57 | // if the answer is right, the question should not be deleted 58 | // Why? 59 | // If the message history is only the answer, no question, it is strange. 60 | delQuestionTask.cancel(); 61 | } 62 | event.getMessage().reply("正确!加 10 分!"); 63 | } 64 | } 65 | } 66 | } 67 | }; 68 | tempListener(l); 69 | } 70 | 71 | public void start() { 72 | runTaskTimer(Main.getInstance(), TimeUnit.MINUTES.toMillis(10), TimeUnit.MINUTES.toMillis(10)); 73 | Main.getInstance().getCore().getEventManager().registerHandlers(Main.getInstance(), this); 74 | } 75 | 76 | private static void tempListener(Listener listener) { 77 | Main.getInstance().getCore().getEventManager().registerHandlers(Main.getInstance(), listener); 78 | new JKookRunnable() { 79 | @Override 80 | public void run() { 81 | Main.getInstance().getCore().getEventManager().unregisterHandlers(listener); 82 | } 83 | }.runTaskLater(Main.getInstance(), TimeUnit.MINUTES.toMillis(1L)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ch_10/code/src/main/java/snw/jkook/example/math/RandomMath.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example.math; 2 | 3 | import javax.script.ScriptEngine; 4 | import javax.script.ScriptEngineManager; 5 | import javax.script.ScriptException; 6 | import java.util.Random; 7 | import java.util.function.Supplier; 8 | 9 | public class RandomMath { 10 | // --- Constants --- 11 | private static final ScriptEngine JS_ENGINE; 12 | 13 | static { 14 | // I don't know how to implement basic calculation in Java :( 15 | // So I choose JavaScript engine! 16 | JS_ENGINE = new ScriptEngineManager().getEngineByName("js"); 17 | } 18 | 19 | // --- Member variables --- 20 | private final Random random; 21 | private final int[] parts; 22 | private final int[] opCodes; 23 | private final String result; 24 | private final String calcString; 25 | 26 | // --- Methods --- 27 | 28 | public RandomMath() { 29 | random = new Random(); 30 | parts = new int[5 + random.nextInt(5)]; 31 | opCodes = new int[parts.length - 1]; 32 | initData(); 33 | calcString = initCalcString(); 34 | result = calcResult(); 35 | } 36 | 37 | 38 | private void initData() { 39 | fill(parts, () -> 1 + random.nextInt(10)); 40 | fill(opCodes, () -> 1 + random.nextInt(4)); 41 | // check / operations 42 | for (int i = 0; i < opCodes.length; i++) { 43 | if (opCodes[i] == 4) { // if divide 44 | while (parts[i] % parts[i + 1] != 0) { 45 | // regenerate parts 46 | parts[i] = 1 + random.nextInt(10); 47 | parts[i + 1] = 1 + random.nextInt(10); 48 | } 49 | if (i != opCodes.length - 1 // if not final round 50 | && opCodes[i + 1] == 4 // if next is still divide 51 | ) { // prevent x/y/z situations 52 | opCodes[i + 1] = (random.nextBoolean() ? 1 : 2); 53 | } 54 | } 55 | } 56 | } 57 | 58 | private String calcResult() { 59 | try { 60 | return JS_ENGINE.eval(getCalcString()).toString(); 61 | } catch (ScriptException e) { 62 | throw new RuntimeException(e); 63 | } 64 | } 65 | 66 | private String initCalcString() { 67 | StringBuilder builder = new StringBuilder(); 68 | boolean t = false; 69 | for (int i = 0; i < parts.length; i++) { 70 | builder.append(parts[i]); 71 | if (i < opCodes.length) { // if this is false, the round is ended after this statement 72 | builder.append(getOperatorString(opCodes[i])); 73 | } 74 | } 75 | return builder.toString(); 76 | } 77 | 78 | public String getResult() { 79 | return result; 80 | } 81 | 82 | public String getCalcString() { 83 | return calcString; 84 | } 85 | 86 | public static void fill(int[] arr, Supplier supplier) { 87 | for (int i = 0; i < arr.length; i++) { 88 | arr[i] = supplier.get(); 89 | } 90 | } 91 | 92 | public static String getOperatorString(int data) { 93 | switch (data) { 94 | case 1: 95 | return "+"; 96 | case 2: 97 | return "-"; 98 | case 3: 99 | return "*"; 100 | case 4: 101 | return "/"; 102 | } 103 | throw new IllegalArgumentException("Unsupported operator type"); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ch_10/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_2/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 2 2 | 3 | 本章介绍 JKook API 的配置系统。 4 | 5 | 希望上一章的代码在你的设备上已经正常运行了。 6 | 7 | 如果说,有人对你的 Hello World 插件产生了兴趣,但是TA想让你的插件输出点不一样的东西,如果要修改代码,岂不是每需要另一种话时就需要修改一次代码?太麻烦了! 8 | 9 | 而通过本章的学习,你将知道如何用配置文件使你的插件变得可配置。 10 | 11 | 首先,我们需要明确一些概念。 12 | 13 | JKook 使用 YAML (1.1 版) 作为 `plugin.yml` 以及配置文件的格式。 14 | 15 | YAML 是以键值对的形式存储数据的。这点和 JSON 很像,也很像 `java.util.Map` 。 16 | 17 | 如 `foo: bar` 中,foo 是键,bar是值。 18 | 19 | 以下为一个实例 (注释会解释这些值的类型): 20 | ```yml 21 | a: 1 # int 22 | b: 1.1 # double, 也可以是 float 23 | c: # ConfigurationSection 24 | d: 1.1.1 # String (因为不是一个有效数字), 此配置项的路径是 c.d 25 | e: true # boolean 26 | f: ["a", "b", "c"] # List 27 | ``` 28 | 请注意: YAML 对缩进要求很严格。 29 | 30 | YAML 的完整语法这里不再讲述。 31 | 32 | 当你能理解这些之后,就开始学习下面的内容吧。 33 | 34 | 请打开 `code/src/main/java/snw/jkook/example/Main.java` 文件,并找到第 1 处。 35 | 36 | 在这里,我们调用了 `saveDefaultConfig` 方法,这个方法将从插件的 JAR 包中释放名为 `config.yml` 的文件到插件的数据文件夹。数据文件夹的 `java.io.File` 实例可以通过插件的 `getDataFolder` 方法获得。 37 | 38 | 但是,若插件的数据文件夹中已经有了 `config.yml` ,则 `saveDefaultConfig` 将不会释放配置文件。 39 | 40 | 请注意: 若你的插件有配置文件,请一定只在 `onLoad` 的时候调用 `saveDefaultConfig` 方法,因为一个 JKook API 的实现会在 `onLoad` 之后才加载配置文件到内存,然后调用 `onEnable` 方法。 41 | 42 | 接着,转到第 2 处。 43 | 44 | 我们的重点是其中的参数。 45 | 46 | `getConfig().getString("msg")` 意为从当前插件的配置中获取 "msg" 键的值并返回。 47 | 48 | **注意,当 msg 键在配置文件中未被定义时,`getString` 方法将返回 `null` ,而不是 `null` 的字符串形式,也不是空字符串。** 49 | 50 | 当然,你可以用 `getString("msg", "")` ,这样,当 msg 键在配置文件中未被定义时,将返回 "" (即空字符串) 。其中,第二个参数称之为默认值。 51 | 52 | 纵观 `snw.jkook.config.ConfigurationSection` 接口,你会发现很多的形如 getXXX 的方法,如 `getInt`,`getDouble` 等。 53 | 54 | 它们都有两种重载。 55 | 56 | 第一种是单参数的,当获取不到,或者无法将存在的值解析为你想要的类型时返回一个默认值。(如 `getInt` 的一般默认值为 0,`getDouble` 的一般默认值为 0.0) 57 | 58 | 第二种即为需要自行提供默认值的。 59 | 60 | 另外,此接口也提供了一些方法,它们能返回 `Collection` 的子类的实例。如 `getXXXList` (具体的有 `getStringList`, `getIntegerList` 等) 。 61 | 62 | 既然 get 方法都有了,有没有 setXXX 的方法? 63 | 64 | 如果你看了第 3 处,你应该知道答案了。没有。 65 | 66 | 直接 `set(path, value)` 即可。其中,path 是配置项的名称,value 是将为此配置项赋予的新值,为 `Object` 类型。 67 | 68 | 如果想要删掉某个配置项,value 传入 `null` 即可。 69 | 70 | 最后,请看第 4 处。 71 | 72 | 这里我们调用了 `saveConfig` 方法,作用是保存内存中的配置数据到插件数据文件夹中的 `config.yml` 文件。 73 | 74 | 如果在插件运行期间,内存中的配置被修改,请务必在 `onDisable` 的时候调用 `saveConfig` 。否则被修改过的配置将丢失。当然,若你的配置不需要保存,可以不写。 75 | 76 | --- 77 | 78 | 现在聊聊运行这个示例的结果吧。 79 | 80 | 假设你是第一次运行这个示例。 81 | 82 | 首先,默认配置文件在 `onLoad` 时被释放,然后在 `onEnable` 前被加载,这时,配置项 `msg` 的值为 "Hello world!" 。 83 | 84 | 但在 `onEnable` 时,配置项 `msg` 的值被设为 `fine` 。 85 | 86 | 又因为 `onDisable` 时,调用了 `saveConfig` 以保存配置,故关闭 API 实现后,在本地的 `config.yml` 中,`msg` 配置项的值为 `fine` 。 87 | 88 | --- 89 | 90 | 本章我们讲解了: 91 | 92 | * YAML 的基本语法 93 | * ConfigurationSection 中的各种方法 94 | * 加载与保存配置数据 -------------------------------------------------------------------------------- /ch_2/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_2/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_2/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.plugin.BasePlugin; 4 | 5 | public class Main extends BasePlugin { 6 | 7 | @Override 8 | public void onLoad() { 9 | saveDefaultConfig(); /* 1 */ 10 | } 11 | 12 | @Override 13 | public void onEnable() { 14 | getLogger().info(getConfig().getString("msg")); /* 2 */ 15 | getConfig().set("msg", "fine"); /* 3 */ 16 | } 17 | 18 | @Override 19 | public void onDisable() { 20 | saveConfig(); /* 4 */ 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ch_2/code/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | msg: "Hello world!" -------------------------------------------------------------------------------- /ch_2/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_3/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 3 2 | 3 | _注意: 为了更好的展示 `Plugin` 接口的内容,本章示例代码使用 **JKook API 0.48** 。_ 4 | 5 | 相信经过前两章的学习,你已经对 JKook 框架有了初步的认识。 6 | 7 | 现在,让我们回过头来,再看看 `Plugin` 接口。 8 | 9 | 当然,也会顺带讲一点与其相关的类。 10 | 11 | 但是,在此之前,先编译一下示例代码,然后拿着工件去一个 JKook API 的实现跑一下! 12 | 13 | 注意观察控制台。 14 | 15 | 跑完之后请打开 `code/src/main/java/snw/jkook/example/Main.java` 文件,然后我们再详解。 16 | 17 | --- 18 | 19 | 开篇而来的是... `getLogger()` 方法,老朋友了,[第 1 章](../ch_1/README.md) 就讲过了。不多讲了。 20 | 21 | 第 1 处。老朋友了,`saveDefaultConfig` 方法我们已经在 [第 2 章](../ch_2/README.md) 里讲过。也不多讲了。 22 | 23 | 第 2 处。 24 | 25 | `Plugin#getDescription` 方法返回的是此插件的描述信息对象(即 `snw.jkook.plugin.PluginDescription` 类的实例)。 26 | 27 | 其本质上是对 `plugin.yml` 的封装。 28 | 29 | --- 30 | 31 | 接下来讲点与 IO 有点关系的方法。 32 | 33 | 第 3 处。 34 | 35 | `Plugin#getFile` 方法返回插件所在的文件对象。 36 | 37 | 比如你可以借此方法实现一次性插件??? 38 | 39 | _~~这个想法很奇怪。我也不知道为什么我会想到这个。~~_ 40 | 41 | ```java 42 | public class Main extends BasePlugin { 43 | @Override 44 | public void onEnable() { 45 | getFile().deleteOnExit(); 46 | } 47 | } 48 | ``` 49 | 50 | 51 | 第 4 处。 52 | 53 | `Plugin#getResource` 方法尝试从插件所在的文件对象中获取一个指定的资源的输入流。 54 | 55 | 其本质是对 `ClassLoader#getResource` 方法的二次封装。 56 | 57 | 你可以通过此方法配合 IO 流实现从插件文件中读取数据,从而让一些数据不需要在代码中硬编码。 58 | 59 | 第 5 处。 60 | 61 | `Plugin#saveResource` 方法是对 `Plugin#getResource` 方法的又一层包装。 62 | 63 | 此方法可以让你将插件文件中的数据文件"解压"到插件的数据文件夹。 64 | 65 | 第 6 处。 66 | 67 | `Plugin#getDataFolder` 方法可以让你获得插件数据文件夹的文件对象。 68 | 69 | 在 KookBC 中,插件数据文件夹一般在 KookBC 所在的目录下的 `plugins` 目录中,以插件 `plugin.yml` 的 `name` 项的值命名。 70 | 71 | 若你的插件数据需要保存,则请保存在这个文件夹下。 72 | 73 | --- 74 | 75 | 其实还有少数几个方法未详解,但因为不常用,故不作详细讲解了,可以自行翻阅 Javadoc 。 76 | * 我也不能什么都讲,有一些东西需要自己探索。 77 | -------------------------------------------------------------------------------- /ch_3/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_3/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_3/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.plugin.BasePlugin; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | public class Main extends BasePlugin { 11 | 12 | @Override 13 | public void onLoad() { 14 | /* 1 */ 15 | saveDefaultConfig(); // save default config file 16 | // This is equals to saveResource("config.yml", false, false); 17 | // But the IllegalArgumentException is ignored 18 | } 19 | 20 | @Override 21 | public void onEnable() { 22 | // The following statements will show you the usage about the methods in Plugin interface. 23 | getLogger().info("Hello world"); 24 | 25 | getLogger().info("My name is {}, version {}", 26 | getDescription().getName(), /* 2 */ 27 | getDescription().getVersion() 28 | ); 29 | getLogger().info("I'm working on JKook API version {}", 30 | getDescription().getApiVersion() 31 | ); 32 | 33 | getLogger().info("I'm at {}", getFile()); /* 3 */ 34 | 35 | InputStream stream = getResource("hello.txt"); /* 4 */ 36 | // You can use the following statements to read data from your plugin JAR: 37 | byte[] streamData; 38 | try { 39 | streamData = inputStreamToByteArray(stream); 40 | } catch (IOException e) { 41 | throw new RuntimeException(e); 42 | } 43 | String data = new String(streamData, StandardCharsets.UTF_8); 44 | assert data.equals( 45 | "This is an example for telling you how to use Plugin#saveResource (or Plugin#getResource) method." 46 | ); 47 | 48 | 49 | saveResource("hello.txt", false, false); /* 5 */ 50 | 51 | getLogger().info("Go to {}, a surprise is appeared there!", getDataFolder()); /* 6 */ 52 | } 53 | 54 | @Override 55 | public void onDisable() { 56 | getLogger().info("Goodbye world"); 57 | } 58 | 59 | public static byte[] inputStreamToByteArray(InputStream input) throws IOException { 60 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 61 | byte[] buffer = new byte[4096]; 62 | int n; 63 | while (-1 != (n = input.read(buffer))) { 64 | output.write(buffer, 0, n); 65 | } 66 | return output.toByteArray(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ch_3/code/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | message: "Hi there!" -------------------------------------------------------------------------------- /ch_3/code/src/main/resources/hello.txt: -------------------------------------------------------------------------------- 1 | This is an example for telling you how to use Plugin#saveResource (or Plugin#getResource) method. -------------------------------------------------------------------------------- /ch_3/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_4/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 4 2 | 3 | 在了解了 JKook API 的插件系统后,本章我们讲讲实体体系。 4 | 5 | 实体 (英文 `Entity`) 一词借鉴于游戏 Minecraft ,这里表示一个可以产生交互的对象。 6 | 7 | JKook API 将对于 KOOK 软件中的"实体"及其附属内容的抽象放在 `snw.jkook.entity` 包。 8 | * 其实**用户发送的消息也算是一种实体**,但是在 API 早期开发阶段的时候,我们发现消息包中的 CardMessage 部分的包名过长(当时是诸如 `snw.jkook.entity.message.component.card.structure` 的名称),故从实体包 `snw.jkook.entity` 移动到了根包 `snw.jkook` 。~~虽然还是很长,但聊胜于无。~~ 9 | * 消息体系将在下章讲解。 10 | 11 | ## 运行一次示例 12 | 13 | 先运行本章的示例代码。 14 | 15 | 本章示例代码有一个 /info 命令,试试在一个文字频道和私信里各执行一次? 16 | 17 | 效果是: 首先,机器人会在你发的命令消息上增加一个 "大笑" 的 Emoji ,然后告诉你一些诸如你的用户 ID ,完整用户名之类信息。 18 | 19 | 在阅读下文的过程中阅读示例代码,你将渐渐理解它。 20 | 21 | ## 总览包结构 22 | 23 | 先总览包结构。 24 | 25 | ```text 26 | snw.jkook.entity 27 | | CustomEmoji 28 | | Game 29 | | Guild 30 | | Invitation 31 | | Reaction 32 | | Role 33 | | User 34 | | 35 | +---abilities 36 | | Accessory 37 | | AccessoryHolder 38 | | AvatarHolder 39 | | InviteHolder 40 | | MasterHolder 41 | | Nameable 42 | | ReactionHolder 43 | | Receivable 44 | | 45 | +---channel 46 | | Category 47 | | Channel 48 | | TextChannel 49 | | VoiceChannel 50 | | 51 | \---mute 52 | MuteData 53 | MuteResult 54 | ``` 55 | 56 | `snw.jkook.entity.abilities` 包中的各种接口表示一个实体可以具有的能力。 57 | 58 | `snw.jkook.entity.channel` 包提供了对服务器(`Guild`)中的各种频道的抽象。 59 | 60 | `snw.jkook.entity.mute` 包是用于服务 `Guild#getMuteStatus` 方法的,其中的接口只是附属内容,本章不会讲解。 61 | 62 | --- 63 | 64 | ## 获取这些元素 65 | 66 | 你可以通过 `snw.jkook.HttpAPI` 接口获得 `User`,`Guild`,`Channel` 的实例。 67 | 68 | 更多的附属元素(如 `CustomEmoji`)可以这些元素中的一些方法获得。 69 | 70 | 你也可以通过一些事件获得这些元素。事件系统的使用我们将在后文讲解。 71 | 72 | ## 具体元素 73 | 74 | ### User 75 | 76 | 完整限定名为 `snw.jkook.entity.User` 。 77 | 78 | 表示一个用户。 79 | * 机器人也是一种用户。机器人的用户对象可以通过 `Core#getUser` 方法获得。 80 | 81 | 假设你现在有一个用户对象。 82 | 83 | 你可以通过 `User#getId` 方法获得这个用户的 KOOK ID ,用户的 KOOK ID 是唯一且不可变的。 84 | 85 | 通过 `User#getName` 方法可以获得这个用户的名称,它是可变的。用户在一个服务器没有昵称时,显示其名称。 86 | * 普通用户 90 天可以修改一次名称,有 KOOK BUFF 的用户 10 天可以修改一次名称。 87 | * KOOK 用户的完整名称格式是 `Name#IdentifyNumber` ,`User#getName` 方法仅能获得前半部分(即 `Name`),若需要用户的完整名称,用表达式 `user.getName() + "#" + user.getIdentifyNumber()` 即可。 88 | 89 | `User#isVip` 方法会告诉你这个用户是否正在享用 KOOK BUFF 权益。 90 | 91 | 获取一个用户在特定服务器的昵称,可以使用 `User#getNickName(Guild)` 方法。 92 | 93 | 更多的属性,诸如是否在线(`User#isOnline`),是否被 KOOK 平台封禁(`User#isBanned`) 等均可通过此接口获得。 94 | 95 | #### 私信 96 | 97 | `User#sendPrivateMessage` 方法用于对此用户发送一条私信消息。 98 | 99 | 具体的用法将在下一章讲解。 100 | 101 | #### 亲密度 102 | 103 | 本节专门讲解 KOOK 为机器人设计的用户亲密度系统。 104 | 105 | > "机器人可以在应用后台配置默认的初始亲密度和形象。机器人可以根据一些逻辑来更新与该用户的亲密度,从而更新形象展示(注: 包含在社交信息中)。" 106 | > 107 | > --- 摘自 KOOK 开发者文档,有修改 108 | 109 | 先上几张图片。 110 | 111 | ![](images/1.png) 112 | 113 | 上图为机器人用户在私聊界面的社交信息按钮点击后展示的内容。 114 | 115 | ![](images/2.png) 116 | 117 | 上图为机器人用户的个人信息页中的社交信息部分。 118 | 119 | ![](images/3.png) 120 | 121 | ![](images/4.png) 122 | 123 | 以上两张图片为机器人应用后台关于亲密度的设置页面。 124 | 125 | 修改用户亲密度的方法为 `User#setIntimacy` 。 126 | 127 | 亲密度的有效范围为 0 -- 2200 ,因此,若传递此范围以外的数字,方法将抛出异常。 128 | 129 | > 正常亲密度是 0-2000 ,在 1000-2000 的范围中每 100 分为 1 颗红心,0-1000 是负分,显示灰色的裂心。 130 | > 131 | > --- 不鲲 (KOOK 员工) 132 | > 133 | > 在 KOOK 官方开发者服务器 "问题反馈" 频道发送的消息,发送于 2020-12-14 下午 09:30,根据 KOOK 客户端现状进行了修改 134 | > 135 | > 注: 虽然 KOOK 客户端将负分渲染为灰色的裂心,但从图 3 可以看出,应用后台遇到负分仍然是渲染为蓝心。 136 | 137 | _~~也许你可以利用这个系统制作一个可以培养好感度的机器人?????哦,这个想法太怪了。~~_ 138 | 139 | #### 好友系统 140 | 141 | 感谢 [DeeChael](https://github.com/DeeChael) 发掘的好友系统 API 。 142 | 143 | 这部分 API 加入于 API 0.49.0 。 144 | 145 | 你的机器人可以和普通用户成为好友了! 146 | 147 | 相关方法放在 `HttpAPI` 中,但是因为这些方法与用户联系更为密切,所以我们放在这里来讲。 148 | 149 | `HttpAPI#getFriendState` 方法可以让你获取当前机器人账号下的好友列表状态,它只是一个快照,这意味着当你需要最新数据时,你应该调用此方法重新请求数据。 150 | 151 | `HttpAPI#addFriend` 方法可以让你向指定用户发出一个好友请求。 152 | 153 | `HttpAPI#deleteFriend` 方法可以让你删除一个好友。 154 | 155 | `HttpAPI#handleFriendRequest` 方法与 `HttpAPI.FriendRequest#handle` 效果一致,决定是否同意好友请求。 156 | 157 | ### Guild 158 | 159 | 完整限定名为 `snw.jkook.entity.Guild` 。 160 | 161 | 表示一个服务器,是 KOOK 软件中用户交流与管理的主要渠道。 162 | 163 | #### Permission 164 | 165 | 完整限定名为 `snw.jkook.Permission` 。 166 | 167 | 对一个服务器进行一些操作会需要机器人拥有特定的权限。 168 | 169 | > 权限是一个 unsigned int 值,由比特位代表是否拥有对应的权限。 170 | > 171 | > 权限值与对应比特位进行按位与操作,判断是否拥有该权限。 172 | > 173 | > --- [KOOK 开发者文档](https://developer.kookapp.cn/doc/http/guild-role) 174 | 175 | 而此枚举存放 KOOK 中各种服务器权限的值。 176 | 177 | 你会在对角色的操作时用到它们。 178 | 179 | Tips: 有一个注解,叫 `snw.jkook.util.RequirePermission` ,它会和 `Permission` 的枚举对象一起出现在一些方法上,作用是提醒你这个方法需要特定权限。 180 | 181 | 判断权限?使用 `Permission.hasPermission` 方法,传入一个权限枚举对象和一个权限值总和即可。 182 | 183 | 那怎么计算权限值呢? 184 | 185 | 将多个权限的值 (值可以通过 Permission 对象的 getValue 方法获得) 相加,即为这几个权限的总和。 186 | 187 | 移除一个权限?先得到权限值总和,判断有无这个权限,如果有,将权限值综合与这个权限的值相减即可。 188 | 189 | 这里提供了一些工具代码: 190 | ```java 191 | public static int sum(Permission... perms) { 192 | int sum = 0; 193 | for (Permission p : perms) { 194 | sum += p.getValue(); 195 | } 196 | return sum; 197 | } 198 | 199 | public static int remove(int sum, Permission perm) { 200 | return Permission.hasPermission(perm, sum) ? sum - perm.getValue() : sum; 201 | } 202 | ``` 203 | 204 | `sum` 方法为求多个权限的值之和。 205 | 206 | `remove` 方法为从给定的权限值中移除指定权限。 207 | 208 | (这些工具代码已经在最新的 API 0.49.0 里提供。) 209 | 210 | #### Role 211 | 212 | **对 `Role` 进行任何操作的前提是你的机器人在角色对应服务器中有角色管理的权限。** 213 | 214 | 完整限定名为 `snw.jkook.entity.Role` 。 215 | 216 | 表示一个服务器中的一个角色。可以为不同的角色配置不同的权限,以实现权限管理的功能。 217 | 218 | 可以通过 `User#grantRole` 和 `User#revokeRole` 方法实现用户角色的授予与剥夺。 219 | 220 | 在一个服务器中新建一个角色?调用 `Guild#createRole` 方法。 221 | 222 | 获取一个服务器已有的所有角色?调用 `Guild#getRoles` 方法。 223 | 224 | 你可以通过 `Role#isPermissionSet` 方法检查此角色是否有特定权限。 225 | 226 | #### Invitation & InviteHolder 227 | 228 | 一个服务器里人太少了怎么办?邀请一些你的朋友! 229 | 230 | 这里讲解 JKook 对于 "邀请" 这一过程中的一些东西是如何抽象的。 231 | 232 | 首先,在 KOOK 中,邀请一个人到一个服务器/频道的方式是向 TA 发送邀请链接。 233 | 234 | 邀请链接不仅可以由用户创建,也可以由机器人创建。~~这简直是废话,毕竟机器人就是一种用户。~~ 235 | 236 | 怎么创建邀请链接?这就要看 `InviteHolder` 了。 237 | 238 | 其完全限定名为 `snw.jkook.entity.abilities.InviteHolder` 。表示一个可以创建邀请链接的对象。 239 | 240 | `Guild`,`NonCategoryChannel` 都是其子类。 241 | 242 | 其代码如下: 243 | 244 | ```java 245 | public interface InviteHolder { 246 | 247 | PageIterator> getInvitations(); 248 | 249 | String createInvite(int validSeconds, int validTimes); 250 | } 251 | ``` 252 | 253 | 在这个接口下,我们能看见一个叫 `createInvite` 的方法。 254 | 255 | 在参数中,前者为有效时间(单位:秒),后者为邀请链接可以被使用的次数。 256 | 257 | 以下给出一些算好的有效时间的常量: 258 | ```text 259 | 0 => 永不 260 | 1800 => 0.5 小时 261 | 3600 => 1 个小时 262 | 21600 => 6 个小时 263 | 43200 => 12 个小时 264 | 86400 => 1 天 265 | 604800 => 7 天 266 | ``` 267 | 268 | 以下给出一些邀请链接可用次数的常量: 269 | ```text 270 | -1 => 无限制 271 | 1 => 1 次使用 272 | 5 => 5 次使用 273 | 10 => 10 次使用 274 | 25 => 25 次使用 275 | 50 => 50 次使用 276 | 100 => 100 次使用 277 | ``` 278 | 279 | 以上常量摘自 [KOOK 开发者文档](https://developer.kookapp.cn/doc/http/invite)。 280 | 281 | 此方法的返回结果是一个邀请链接的字符串,你可以通过这个制作一些诸如通过命令获取特定频道邀请链接的功能。~~(感觉好鸡肋啊)~~ 282 | 283 | 获取为 `InviteHolder` 的实例创建的邀请信息,使用 `getInvitations` 方法。 284 | 285 | 什么?你想问 `PageIterator` 是什么? 286 | 287 | 那是我们对 KOOK HTTP API 中按分页格式设计的 API 的统一抽象。因为按分页格式设计的 API 不能一次得到所有数据,需要遍历,因此我们创建了 `PageIterator` 。 288 | 289 | 就当它是一个普通的 `Iterator` 用吧。 290 | 291 | 还不知道 `Iterator` 是什么?看看[这个](https://www.liaoxuefeng.com/wiki/1252599548343744/1265124784468736)? 292 | * 此链接来自 廖雪峰的官方网站 ,感谢作者提供的优秀文章! 293 | 294 | ### Channel 295 | 296 | 完整限定名为 `snw.jkook.entity.channel.Channel` 。 297 | 298 | 表示一个频道。 299 | 300 | 获取一些常规属性的方法(如名称对应 `getName` 方法,ID 对应 `getId` 方法)这里就不多讲了。 301 | 302 | ~~`Channel#getParent` 方法可以获取频道的父分组频道对象。~~ 303 | 304 | ~~有对应的 `Channel#setParent` 方法可以修改。~~ 305 | 306 | * 自 API 0.49.0 开始,因为 `Category` 不可能有父分组,所以将与父分组有关的方法移到了新接口 `NonCategoryChannel` 。 307 | * 如果你需要调用相关方法,请自行向下转型。 308 | 309 | #### Category 310 | 311 | 完整限定名为 `snw.jkook.entity.channel.Category` 。 312 | 313 | 表示一个分组。 314 | 315 | 分组是什么? 316 | 317 | ![](images/5.png) 318 | 319 | 图中箭头所指的就是一个分组。 320 | 321 | **关于分组有一个特性:对分组设置的[特定于频道的权限](#特定于频道的权限)会对其下属的所有频道生效。除非频道的权限不与分组同步。** 322 | 323 | ![](images/6.png) 324 | 325 | **另外注意,来自 `InviteHolder` 接口的方法以及 `Channel#getParent`、`Channel#setParent` 方法在 `Category` 接口中不可用,因为它不是 `NonCategoryChannel` 。** 326 | 327 | #### NonCategoryChannel 328 | 329 | 完全限定名为 `snw.jkook.entity.channel.NonCategoryChannel` 。 330 | 331 | 加入于 API 0.49.0 。 332 | 333 | 表示一个"不是分组"的频道,设计它是为了将一些在 API 0.48 及以前对分组完全不可用的功能剥离出来。 334 | 335 | 是 `InviteHolder` 的子类,这意味着有关邀请链接的方法对此接口可用。 336 | 337 | 是 `ParentHolder` (`snw.jkook.entity.abilities.ParentHolder`) 的子类,这意味着与父分组有关的方法 (`getParent`, `setParent` 方法) 对此接口可用。 338 | 339 | `TextChannel` 与 `VoiceChannel` 都是其子类。 340 | 341 | #### TextChannel 342 | 343 | 完整限定名为 `snw.jkook.entity.channel.TextChannel` 。 344 | 345 | 表示一个文字频道。 346 | 347 | 获取一个文字频道中的历史消息可以使用 `TextChannel#getMessages` 方法。使用此方法建议配合阅读[KOOK 开发者文档 - 消息](https://developer.kookapp.cn/doc/http/message)中"获取频道聊天消息列表"一节。 348 | 349 | 对一个文字频道发送消息组件可以使用 `TextChannel#sendComponent` 方法。具体用法将在下章讲解。 350 | 351 | #### VoiceChannel 352 | 353 | 完整限定名为 `snw.jkook.entity.channel.VoiceChannel` 。 354 | 355 | 表示一个语音频道。 356 | 357 | `VoiceChannel#getUsers` 方法可以获取由所有已加入此语音频道的用户组成的列表。 358 | 359 | `VoiceChannel#moveToHere` 方法可以将另一个语音频道中的用户移动到此语音频道。 360 | 361 | `VoiceChannel#getMaxSize` 方法可以获取此语音频道最大可以容纳的用户数。 362 | 363 | #### 特定于频道的权限 364 | 365 | 一个角色的权限适用于整个服务器的所有频道。 366 | 367 | 但是通过为特定频道给特定角色/用户设置权限,可以给这些被特别对待的角色/用户在特定的频道本没有的权限。 368 | 369 | 举个例子。 370 | 371 | 用户 A 有角色 B 。 372 | 373 | 有一个频道需要有角色 C 的用户才能访问。 374 | 375 | 但是管理员可以在这个频道上特别设置用户 A 的权限,使其可以访问这个频道。 376 | 377 | 再举一个例子。 378 | 379 | 有一个文字频道,所有人都不能在那里上传文件。 380 | 381 | 但是管理员可以特别让有角色 D 的用户可以在这里上传文件。 382 | 383 | 我们将这称之为特定于频道的权限。 384 | 385 | 在 `Channel` 接口中,有如下方法可以操作特定于频道的权限。 386 | 387 | ```java 388 | public interface Channel { 389 | 390 | // ... 391 | 392 | void updatePermission(int roleId, int rawAllow, int rawDeny); 393 | 394 | void updatePermission(Role role, int rawAllow, int rawDeny); 395 | 396 | void updatePermission(User user, int rawAllow, int rawDeny); 397 | 398 | void deletePermission(Role role); 399 | 400 | void deletePermission(User user); 401 | 402 | // ... 403 | 404 | } 405 | ``` 406 | 407 | 前两个是对角色设置特定于频道的权限,第三个是对用户设置特定于频道的权限。 408 | 409 | 最后两个是删除频道上指定角色/用户的特定于频道的权限。 410 | 411 | ### CustomEmoji 412 | 413 | 完整限定名为 `snw.jkook.entity.CustomEmoji` 。 414 | 415 | 表示一个表情(也可以直接称作 Emoji)。 416 | 417 | 其 ID 可以通过 `CustomEmoji#getId` 方法获取。 418 | 419 | 它可以是: 420 | 421 | ![](images/7.png) 422 | 423 | 这些由 KOOK 提供的自带 Emoji 。下文称之为 "KOOK 原生 Emoji"。 424 | 425 | 也可以是: 426 | 427 | ![](images/8.png) 428 | 429 | 这种由服务器管理员上传的 Emoji 。 430 | 431 | 获取一个服务器的 Emoji 列表可以使用 `Guild#getCustomEmojis` 方法。 432 | 433 | 获取 KOOK 原生 Emoji 的方法如下。 434 | 435 | 到 [KOOK 原生 Emoji 列表 - Kook.Net 文档](https://kooknet.dev/guides/emoji/emoji-list.html) 查询你需要的 Emoji ,获得其 Unicode 码,然后使用 `snw.jkook.Unsafe#getCustomEmoji` 方法获得其具体实例。 436 | * 如 `:smile:` 表情的 Unicode 码为 `\ud83d\ude04` 。 437 | 438 | ![](images/9.png) 439 | 440 | 上图中,向左箭头指向的是 Unicode 码,向右箭头指向的是短代码,用于包含在 Markdown 消息组件中实现发送带 Emoji 的消息。 441 | 442 | 获取 KOOK 原生 Emoji 的大致代码如下: 443 | ```java 444 | String emojiUnicode = ""; 445 | snw.jkook.Unsafe unsafe; // 具体获取 Unsafe 的代码已忽略。 446 | // 如可以使用 JKook.getUnsafe() 447 | // 高版本可以用 Plugin.getCore().getUnsafe() 448 | snw.jkook.entity.CustomEmoji emoji = unsafe.getCustomEmoji(emojiUnicode); 449 | ``` 450 | 451 | `CustomEmoji` 一般配合 `MarkdownComponent`(Markdown 消息组件,下一章会讲),实现机器人发送服务器表情的功能。 452 | 453 | 也可以用于 `Message#sendReaction` 方法,将 Emoji 作为对消息的回应发出。 454 | 455 | ### Reaction 456 | 457 | 完整限定名为 `snw.jkook.entity.Reaction` 。 458 | 459 | 表示对消息的回应。 460 | 461 | 这个也会在下一章讲解。 462 | 463 | --- 464 | 465 | 至此,本章结束了。 466 | 467 | 了解 JKook API 的实体体系,是你编写 JKook 插件的关键。 468 | 469 | 请务必反复仔细阅读本章。 470 | -------------------------------------------------------------------------------- /ch_4/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_4/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_4/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.JKook; 4 | import snw.jkook.command.JKookCommand; 5 | import snw.jkook.entity.CustomEmoji; 6 | import snw.jkook.message.component.MarkdownComponent; 7 | import snw.jkook.plugin.BasePlugin; 8 | 9 | public class Main extends BasePlugin { 10 | 11 | @Override 12 | public void onEnable() { 13 | new JKookCommand("info") 14 | .executesUser((sender, arguments, message) -> { // sender is a User in this case 15 | if (message == null) return; // ignore CommandManager#executeCommand calls 16 | 17 | CustomEmoji smile = JKook.getCore().getUnsafe().getEmoji("\ud83d\ude04"); 18 | message.sendReaction(smile); 19 | 20 | StringBuilder replyContent = new StringBuilder(); 21 | 22 | String name = sender.getName(); // sender's name 23 | String userId = sender.getId(); // sender's ID 24 | String fullName = name + "#" + sender.getIdentifyNumber(); // sender's full name 25 | 26 | replyContent.append("你的名字: ").append(name).append("\n"); 27 | replyContent.append("你的用户 ID: ").append(userId).append("\n"); 28 | replyContent.append("你的完整用户名称: ").append(fullName); 29 | 30 | message.reply(new MarkdownComponent(replyContent.toString())); 31 | }) 32 | .register(this); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ch_4/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_4/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/1.png -------------------------------------------------------------------------------- /ch_4/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/2.png -------------------------------------------------------------------------------- /ch_4/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/3.png -------------------------------------------------------------------------------- /ch_4/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/4.png -------------------------------------------------------------------------------- /ch_4/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/5.png -------------------------------------------------------------------------------- /ch_4/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/6.png -------------------------------------------------------------------------------- /ch_4/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/7.png -------------------------------------------------------------------------------- /ch_4/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/8.png -------------------------------------------------------------------------------- /ch_4/images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_4/images/9.png -------------------------------------------------------------------------------- /ch_5/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 5 2 | 3 | 在你阅读上一章的时候,你应该注意到我们提了几次 `Message` ,那就是我们对 KOOK 消息的抽象接口。 4 | 5 | 本章,我们讲讲 JKook API 的消息体系。 6 | 7 | ## 运行一次示例 8 | 9 | 先运行本章的示例代码。 10 | 11 | 本章示例代码提供了两个命令,分别是 `/msginfo` 和 `/deleteme` 。 12 | 13 | 前者的效果是: 如果命令在文字频道中执行,会顺带提供频道 ID ,频道名称,频道所在服务器 ID ,频道所在服务器的名称。 14 | 15 | 后者的效果是: 如果命令在文字频道中执行,它会删除你发送的 `/deleteme` 消息。仅此而已。 16 | 17 | 在阅读下文的过程中阅读示例代码,你将渐渐理解它。 18 | * _~~这句话好像在哪听过一次?~~_ 19 | * _~~是的,如果你读过上一章,你会发现本节与上一章关于示例代码的介绍出奇地像,因为我是照抄的。~~_ 20 | 21 | ## 消息概述 22 | 23 | 消息是由 KOOK 用户发送的,包含了一些内容的实体。 24 | 25 | **消息是一种实体。** 26 | 没有放在 `snw.jkook.entity` 包的原因在上一章已经讲过了。 27 | 28 | JKook API 消息体系中的各种类基本统一放在 `snw.jkook.message` 包下。 29 | 30 | ## 总览包结构 31 | 32 | 先总览包结构。 33 | 34 | ```text 35 | snw.jkook.message 36 | | Message 37 | | PrivateMessage 38 | | TextChannelMessage 39 | | 40 | \---component 41 | | BaseComponent 42 | | FileComponent 43 | | MarkdownComponent 44 | | TextComponent 45 | | 46 | \---card 47 | | CardBuilder 48 | | CardComponent 49 | | CardScopeElement 50 | | MultipleCardComponent 51 | | Size 52 | | Theme 53 | | 54 | +---element 55 | | BaseElement 56 | | ButtonElement 57 | | ImageElement 58 | | InteractElement 59 | | MarkdownElement 60 | | PlainTextElement 61 | | 62 | +---module 63 | | ActionGroupModule 64 | | BaseModule 65 | | ContainerModule 66 | | ContextModule 67 | | CountdownModule 68 | | DividerModule 69 | | FileModule 70 | | HeaderModule 71 | | ImageGroupModule 72 | | InviteModule 73 | | package-info 74 | | SectionModule 75 | | 76 | \---structure 77 | BaseStructure 78 | Paragraph 79 | ``` 80 | 81 | 根据以上结构,本章我们分为三大部分: 82 | 83 | 消息对象,消息组件,CardMessage(卡片消息)。 84 | 85 | ## 获取消息对象 86 | 87 | 最常见的方法是通过 `ChannelMessageEvent`(用户在文字频道发送消息的事件)或 `PrivateMessageReceivedEvent`(用户给自己发送私信的事件)的 `getMessage` 方法获得消息对象。 88 | * 事件系统将在第 7 章讲解。 89 | 90 | 或者通过命令系统,`CommandExecutor` 和 `UserCommandExecutor` 接口的 `onCommand` 方法均提供了 `Message` 对象,表示导致命令被执行的消息对象。 91 | 92 | 自 API 0.49.0 开始,你可以拿着消息 ID 直接去 `HttpAPI` 接口获取完整消息信息了。 93 | 94 | 查询文字频道消息用 `HttpAPI#getTextChannelMessage` 方法。 95 | 96 | 查询私聊消息信息用 `HttpAPI#getPrivateMessage` 方法。 97 | * 注意,此方法另外需要用户对象。 98 | 99 | ## 消息对象 100 | 101 | 消息对象目前有两种:`TextChannelMessage` 和 `PrivateMessage` 。它们有一个共同的父类是 `Message` 接口。 102 | 103 | ### Message 104 | 105 | 完整限定名为 `snw.jkook.message.Message` 。 106 | 107 | 作为对一个消息的基本抽象,此接口可以获取的内容比较多。其子类主要是提供一些功能扩展。 108 | 109 | 通过 `Message#getId` 方法可以获得此消息的 ID 。 110 | 111 | 消息 ID 是一个 UUID 。 112 | 113 | 通过 `Message#getSender` 方法可以获得此消息的发送者。 114 | 115 | 通过 `Message#getTimeStamp` 方法可以获得此消息被发送时的时间戳。 116 | 117 | 通过 `Message#getComponent` 方法可以获得此消息包含的消息组件。通过消息组件对象,你可以得知用户发送的内容。消息组件相关内容将在下文讲解。 118 | 119 | 更新一条消息的内容可以使用 `Message#setComponent` 方法,但只支持更新 KMarkdown 消息以及卡片消息。 120 | **只能更新机器人自己发出的消息。** 121 | 122 | 删除一条消息可以使用 `Message#delete` 方法。在私信中只能删除自己的消息,在文字频道中删除其他人的消息需要自己有消息管理权限。 123 | 124 | 在一条消息被删除后,对其对应的消息对象进行更新等操作将失败,因此,已被删除的消息的消息对象不再具有可用性。 125 | 126 | `TextChannelMessage` 主要提供了更多的功能,如向频道发送一个临时消息的快捷封装,以及获取消息所在的文字频道。 127 | 128 | ## 消息组件 129 | 130 | 消息组件(英文 `Component`)一词同样借鉴于游戏 Minecraft ,用于存放消息的内容。 131 | * _为什么实体和消息组件的名称都借鉴于 Minecraft ?因为 JKook 的作者就是个 Minecraft 玩家。_ 132 | 133 | JKook API 中的消息组件全部放在了 `snw.jkook.message.component` 包下。 134 | 135 | `BaseComponent` 为消息组件的顶级父类。 136 | 137 | `TextComponent` 为纯文本消息组件。 138 | * **KOOK 官方已经弃用纯文本消息。** 所有的纯文本消息会在 KOOK 的服务端转换为 Markdown 之后再发送。 139 | 140 | `MarkdownComponent` 为 Markdown 消息组件。继承自 `TextComponent` 类。 141 | 142 | `FileComponent` 表示一个文件消息组件。 143 | 144 | `FileComponent` 类下提供了一个 `Type` 枚举,表明文件的类型,支持 `AUDIO`(音乐),`VIDEO`(视频),`IMAGE`(图片),`FILE`(普通文件)。 145 | 146 | 你可以根据文件的类型,在构造它的时候指定类型。不同的类型在 KOOK 客户端中渲染的效果不一样。 147 | 148 | **发送音乐文件时更推荐使用卡片消息中的 `FileModule` ,可以指定封面图片。** 149 | 150 | ## 发送消息 151 | 152 | 通常地,向一个文字频道发送一个消息组件只需要使用 `TextChannel#sendComponent` 方法即可。 153 | 154 | 向一个用户的私信发送一个消息组件?使用 `User#sendPrivateMessage` 方法即可。 155 | 156 | ### 回复消息 157 | 158 | 回复消息有两种方式。 159 | 160 | 1. 获取消息的来源(在私信中指用户,在文字频道中就是文字频道本身),然后调用相应的发送消息的方法 161 | 2. 直接使用 `Message#reply` 。 162 | 163 | 使用第一个方法可以这么写: 164 | ```java 165 | BaseComponent component; 166 | Message message; 167 | if (message instanceof TextChannelMessage) { 168 | ((TextChannelMessage) message).getChannel().sendComponent(component, message, null); 169 | } else { 170 | message.getSender().sendPrivateMessage(component, message); 171 | } 172 | ``` 173 | 174 | 但这种方法如果想要大量使用必须封装,否则会造成很多重复代码。 175 | 176 | 但是,第二个方法是由 API 直接提供封装。 177 | 178 | 只需要一行代码: 179 | ```java 180 | message.reply(component); 181 | ``` 182 | 183 | ### 发送消息到消息来源 184 | 185 | 如果你不想要那个回复的框框... 186 | 187 | ![](images/1.png) 188 | 189 | 对,就是红框里的那个。 190 | _~~请不要在意内容。~~_ 191 | 192 | 你可以使用另一个由 `Message` 提供的便利方法: `Message#sendToSource` 。 193 | 194 | 只需要一行代码: 195 | ```java 196 | message.sendToSource(component); 197 | ``` 198 | 199 | ### 临时消息 200 | 201 | 这是一个神奇的东西。 202 | 203 | 一个临时消息具有如下特性: 204 | * 只能出现在文字频道中 205 | * 仅有指定的用户可以看见 206 | * 临时消息会在指定的用户在重启 TA 的 KOOK 客户端后消失 207 | * 不可更新 208 | 209 | 因为第一条特性,发送一个临时消息需要将 `Message` 对象转为 `TextChannelMessage` 对象,然后再调用相关方法。 210 | * 请务必先对消息对象进行 `instanceof` 检查。 **永远不要做未经检查的向下转换。** 211 | 212 | 我们支持回复消息的同时把消息作为临时消息,也支持直接发送到消息来源的同时设置为临时消息。 213 | 214 | 对于前者的情况,使用 `TextChannelMessage#replyTemp` 方法,对于后者的情况,使用 `TextChannelMessage#sendToSourceTemp` 方法。 215 | 216 | ## CardMessage 217 | 218 | > 卡片消息是一种结构化的消息,提供了一种易用、统一的富交互形式。 219 | > 220 | > --- KOOK 开发者文档,有修改 221 | 222 | 在阅读本节前,建议先阅读 KOOK 开发者文档中对于 [CardMessage](https://developer.kookapp.cn/doc/cardmessage) 的介绍,然后体验一下 KOOK 的 [卡片消息编辑器](https://kookapp.cn/tools/message-builder.html#/card) 。 223 | 224 | 卡片消息有如下特性: 225 | * 美观 226 | * 提供按钮作为新的交互形式 227 | * 一条卡片消息中可以存放 5 张卡片,一条卡片消息中最多可以有 50 个模块(Module)。 **"卡片消息"在这里不可和"卡片"混为一谈。** 228 | 229 | JKook API 中关于 CardMessage 的内容放在了 `snw.jkook.message.component.card` 包下。 230 | 231 | 这里先讲解几个重要的类: `CardComponent`,`MultipleCardComponent`,`Theme`,`Size` 以及 `CardBuilder` 。 232 | 233 | `CardComponent` 表示单个卡片组件。 234 | 235 | `MultipleCardComponent` 是一个可存放最多 5 张卡片的容器组件。 236 | 237 | `Theme` 枚举列出了 KOOK 支持的几种 "风格" ,它可以影响卡片本身以及特定元素的颜色。 238 | 239 | `Size` 枚举存放了 Bootstrap 中的 4 种栅格布局。 240 | * 卡片本身仅支持 `LG` 与 `SM` 。剩余的是为卡片模块准备的。如果你不知道卡片使用什么大小好,那就去用 `LG` 吧。 241 | * 卡片本身在移动端 KOOK 只会使用 `SM` 大小。 242 | * Bootstrap 是前端框架,不在本教程范围中,只是 KOOK 用到了,故此处不作详细讲解。 243 | 244 | `CardBuilder` 是一个基于建造者模式设计的卡片构造工具类。 245 | 246 | ### 各种元素 247 | 248 | 所有可以组成卡片的元素的顶级父类为 `CardScopeElement` ,位于 `snw.jkook.message.component.card` 包。 249 | 250 | 根据 KOOK 开发者文档,我们将组成卡片的各种元素分为了 `Element` (基础元素),`Module` (模块),`Structure` (结构体)。 251 | 252 | 本节不再详细讲解元素的结构,各种元素类均是按照 KOOK 开发者文档封装的,阅读 KOOK 开发者文档即可。 253 | 254 | #### Accessory 255 | 256 | 其完整限定名为 `snw.jkook.entity.abilities.Accessory` 。 257 | 258 | `Accessory` 表示一种可以嵌入进其他卡片元素的元素。 259 | 260 | 如 `ButtonElement` 即为一种 `Accessory` 。 261 | 262 | 举两个例子: 263 | 264 | ![](images/2.png) 265 | 266 | ![](images/3.png) 267 | 268 | 可以嵌入其他卡片元素的元素为 `AccessoryHolder` ,目前只有 `SectionModule` 是其子类。 269 | 270 | ### CardBuilder 271 | 272 | 本节讲解 `CardBuilder` 的基本用法。 273 | 274 | 在 `CardBuilder` 类的源代码中,我们已经提供了一个小例子,它的源代码如下(有稍作改动): 275 | 276 | ```java 277 | MultipleCardComponent card = new CardBuilder() 278 | .setTheme(Theme.PRIMARY) 279 | .setSize(Size.LG) 280 | .addModule(new HeaderModule(new PlainTextElement("This is header", false))) 281 | .addModule(new SectionModule(new PlainTextElement("This is body"), null, null)) 282 | .newCard() 283 | .setTheme(Theme.DANGER) 284 | .setSize(Size.LG) 285 | .addModule(new HeaderModule(new PlainTextElement("This is header of the second card", false))) 286 | .addModule(new SectionModule(new PlainTextElement("This is body of the second card"), null, null)) 287 | .build(); 288 | ``` 289 | 290 | 这个示例在 KOOK 消息编辑器中的渲染效果为下图: 291 | 292 | ![](images/4.png) 293 | -------------------------------------------------------------------------------- /ch_5/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_5/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_5/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.command.JKookCommand; 4 | import snw.jkook.entity.Guild; 5 | import snw.jkook.entity.channel.TextChannel; 6 | import snw.jkook.message.PrivateMessage; 7 | import snw.jkook.message.TextChannelMessage; 8 | import snw.jkook.message.component.MarkdownComponent; 9 | import snw.jkook.plugin.BasePlugin; 10 | 11 | public class Main extends BasePlugin { 12 | 13 | @Override 14 | public void onEnable() { 15 | new JKookCommand("msginfo") 16 | .executesUser((sender, arguments, message) -> { 17 | if (message != null) { 18 | StringBuilder contentBuilder = new StringBuilder(); 19 | String messageId = message.getId(); 20 | long timestamp = message.getTimeStamp(); 21 | contentBuilder.append("消息 ID: ").append(messageId).append("\n"); 22 | contentBuilder.append("消息发送时的时间戳: ").append(timestamp); 23 | 24 | // In JKook API version 0.51.0 and later, 25 | // you should use "ChannelMessage" instead of "TextChannelMessage" 26 | // because KOOK allows typing in voice channels. 27 | // eg: if (message instanceof ChannelMessage) { } 28 | if (message instanceof TextChannelMessage) { // if this command was executed in a channel 29 | contentBuilder.append("\n"); // ignore this please, just for better look 30 | 31 | // Since KOOK's message events don't specify channel types, 32 | // when using JKook API version 0.51.0 and later, 33 | // you should use NonCategoryChannel instead of TextChannel 34 | // eg: NonCategoryChannel channel = ((ChannelMessage) message).getChannel(); 35 | TextChannel channel = ((TextChannelMessage) message).getChannel(); 36 | Guild guild = channel.getGuild(); 37 | String guildName = guild.getName(); 38 | String guildId = guild.getId(); 39 | String channelName = channel.getName(); 40 | String channelId = channel.getId(); 41 | contentBuilder.append("此消息所在的服务器的名称: ").append(guildName).append("\n"); 42 | contentBuilder.append("此消息所在的服务器的 ID: ").append(guildId).append("\n"); 43 | contentBuilder.append("此消息所在的频道的名称: ").append(channelName).append("\n"); 44 | contentBuilder.append("此消息所在的频道的 ID: ").append(channelId); 45 | } 46 | 47 | message.reply(new MarkdownComponent(contentBuilder.toString())); 48 | } 49 | }) 50 | .register(this); 51 | 52 | // This command can just delete your message. 53 | new JKookCommand("deleteme") 54 | .executesUser((sender, arguments, message) -> { 55 | // use ChannelMessage instead of TextChannelMessage in 0.51.0 + 56 | if (message instanceof TextChannelMessage) { 57 | message.delete(); 58 | } else if (message instanceof PrivateMessage) { // You can't delete messages in private chat 59 | message.reply(new MarkdownComponent("我无法删除你的消息。lol")); 60 | } 61 | }) 62 | .register(this); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ch_5/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_5/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_5/images/1.png -------------------------------------------------------------------------------- /ch_5/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_5/images/2.png -------------------------------------------------------------------------------- /ch_5/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_5/images/3.png -------------------------------------------------------------------------------- /ch_5/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_5/images/4.png -------------------------------------------------------------------------------- /ch_6/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 6 2 | 3 | 命令是 Bot 和用户的重要交流渠道。 4 | 5 | 其实所谓命令,只是一种规范化的消息。 6 | 7 | 本章,我们将介绍 JKook API 的命令系统用法。 8 | 9 | 本章基于两个 API 大版本编写: 0.37 LTS, 0.48 。 10 | 11 | 两个版本之间不同的部分将在下文提及。 12 | 13 | 本章的代码示例基于 API 0.48 编写。下文中的代码亦是如此。 14 | 15 | ## 总纲 16 | 17 | JKook API 命令系统相关的类均可以在 `snw.jkook.command` 包中找到。 18 | 19 | 此包的结构如下: 20 | 21 | ```text 22 | snw.jkook 23 | | 24 | +---command 25 | JKookCommand 26 | CommandException 27 | CommandExecutor 28 | CommandManager 29 | CommandSender 30 | ConsoleCommandSender 31 | ConsoleCommandExecutor (自 API 0.38 开始) 32 | OptionalArgumentContainer (自 API 0.38 开始) 33 | UserCommandExecutor (自 API 0.38 开始) 34 | ``` 35 | 36 | ## JKookCommand 37 | 38 | 这是 JKook API 命令系统的核心类。 39 | 40 | 表示一个命令。 41 | 42 | 是建造者模式。 43 | 44 | 在 JKook API 命令系统中,一个命令可以拥有如下属性: 45 | * 名称 46 | * 前缀 47 | * 别称 48 | * 简介 49 | * 帮助信息 50 | * 执行器 51 | * 参数类型 (自 API 0.38 开始) 52 | 53 | 下文将详细讲解上文所列出的属性。 54 | 55 | ### 命令名称 56 | 57 | 命令名称通过 JKookCommand 的构造方法指定,唯一且不可更改。 58 | 59 | **命令名称、别称、前缀不可以有空格。前缀可以是空字符串,其他两个不能。** 60 | 61 | JKookCommand 有如下的多种构造方法: 62 | ```java 63 | public final class JKookCommand { 64 | 65 | // 最简单的构造器,只需要一个名称。 66 | // 前缀此时使用默认值 "/" 67 | public JKookCommand(String rootName) { 68 | } 69 | 70 | // 在提供命令名称的同时提供一个 char 作为此命令的前缀。 71 | // 此时命令的前缀只有给定的 char 72 | public JKookCommand(String rootName, char prefix) { 73 | } 74 | 75 | // 与 JKookCommand(String, char) 类似,但是因为第二个参数是 String 76 | // 所以你可以指定一个无空格的字符串作为命令前缀 77 | public JKookCommand(String rootName, String prefix) { 78 | } 79 | 80 | // 主构造方法 81 | // 因为第二个参数是 Collection ,所以你可以提供多个字符串作为命令前缀 82 | public JKookCommand(String rootName, Collection prefixes) { 83 | } 84 | 85 | // ... 86 | // 更多方法已忽略 87 | } 88 | ``` 89 | 90 | ### 前缀 91 | 92 | 前缀有助于将用户的正常发言与命令区分开。 93 | 94 | 比如 `查询什么呢?` 在一般人看来就是一个简单的问题,但是如果一个命令就是 `查询什么呢?` ?这会影响用户体验。 95 | 96 | 前缀不仅可以通过构造方法指定,也可以通过 `JKookCommand#addPrefix` 方法添加。 97 | 98 | 对于以下两段代码: 99 | 100 | ```java 101 | new JKookCommand("eg", ".") 102 | // more command code 103 | ``` 104 | 105 | ```java 106 | new JKookCommand("eg") 107 | .addPrefix(".") 108 | // more building code 109 | ``` 110 | 111 | 两者的区别是,前者的前缀只有 "." ,而后者同时有 "/" 和 "."。 112 | 113 | 所以若不希望使用 "/" 作为命令前缀,则需要 `new` 命令对象时就在构造方法指定所有的命令前缀。 114 | 115 | ### 别称 116 | 117 | 命令别称的作用正如其名,对命令的另一种称呼,比如: 118 | 119 | ```java 120 | new JKookCommand("eg") 121 | .addAlias("ab") 122 | // more building code 123 | ``` 124 | 125 | 以上代码表示的命令既可以通过发送 `/eg` 执行,也可以通过发送 `/ab` 执行。 126 | 127 | 命令别称可以通过 `JKookCommand#addAlias` 方法添加。 128 | 129 | ### 简介 & 帮助信息 130 | 131 | 简介和帮助信息不同的是,简介应该在 /help 命令 (由 JKook API 的实现提供) 的结果中展示,并且应该尽可能的简短。 132 | 133 | 帮助信息应该在用户使用 /help 命令的同时指定了此命令的名称 (如 /help eg 即 eg 命令被指定) 时展示。 134 | 135 | ### 注册 136 | 137 | **在你对命令对象完成各项设置后,请务必调用 `JKookCommand#register` 方法!** 138 | 139 | 注意: 140 | * 在 API 0.37 中,`register` 方法不需要参数。 141 | * **在 API 0.48 中,`register` 方法需要一个 `Plugin` 作为命令的所有者。** 142 | * 命令的属性在注册后不能更改。 143 | 144 | --- 145 | 146 | 执行器将在下文详细讲解,详见 [执行器](#执行器) 一节。 147 | 148 | 一个简单的 JKookCommand 可以在本章的示例代码的第 1 处找到。 149 | 150 | ## 执行器 151 | 152 | JKook API 中有 3 种命令执行器。 153 | 154 | 我们按时间顺序来讲。 155 | 156 | **建议先读完下文的 `CommandExecutor` 一节再读另外两节。** 157 | 158 | ### CommandExecutor 159 | 160 | 最早的 JKook API 命令系统使用 `CommandExecutor` 类作为命令执行器。 161 | 162 | 它的声明如下: 163 | 164 | ```java 165 | public interface CommandExecutor { 166 | 167 | void onCommand(CommandSender sender, Object[] arguments, @Nullable Message message); 168 | 169 | } 170 | ``` 171 | 172 | 提供了命令执行者,参数,以及消息对象。 173 | 174 | `CommandSender` 的直接子类有 `User` 和 `ConsoleCommandSender` ,前者表示用户,后者表示控制台。 175 | 176 | **注意,`arguments` 在 API 0.37 和 API 0.48 中类型不一样。** 177 | 178 | * 在 API 0.37 中,它是 `String[]` 类型。 179 | * 在 API 0.48 中,它是 `Object[]` 类型。因为引入了参数解析系统。见下文 [参数解析系统](#参数解析系统) 一节。 _其实自 API 0.38 开始就已经是 `Object[]` 类型了,但是本教程不讲解旧版本的或非 LTS 版本的 API 。_ 180 | 181 | 消息对象在命令执行者是 `User` 时一般不会是 `null` 。 182 | 183 | **但是有个例外,命令在通过将 `User` 传给了 `CommandManager#executeCommand` 方法执行时,得到的 `message` 参数的值为 `null` 。** 184 | 185 | 为一个命令对象设置 `CommandExecutor` 可以调用 `JKookCommand#setExecutor` 方法。 186 | 187 | --- 188 | 189 | 然而,这个执行器设计有一个问题,若一个命令执行器对 `User` 和 `ConsoleCommandSender` 有不同的处理代码,则需要先做 `instanceof` 检查,这样的设计不利于维护。 190 | 191 | 最重要的是,很多时候,命令都是直接由 KOOK 用户执行的,仅控制台可用的命令较少。 192 | 193 | 所以,为了解决这个问题,我们引入了 `UserCommandExecutor` 和 `ConsoleCommandExecutor` ,下文将作讲解。 194 | 195 | ### UserCommandExecutor 196 | 197 | `UserCommandExecutor` 接口与上文的 `CommandExecutor` 接口的区别主要就是 `sender` 的类型是 `User` 。`arguments`参数的差异见上文。 198 | 199 | 为一个命令对象设置 `UserCommandExecutor` 可以调用 `JKookCommand#executesUser` 方法。 200 | 201 | ### ConsoleCommandExecutor 202 | 203 | `ConsoleCommandExecutor` 接口与上文的 `CommandExecutor` 接口的区别主要就是 `sender` 的类型是 `ConsoleCommandSender` 。`arguments`参数的差异见上文。 204 | 205 | 为一个命令对象设置 `ConsoleCommandExecutor` 可以调用 `JKookCommand#executesConsole` 方法。 206 | 207 | ## 子命令 208 | 209 | 对于一个命令对象,其可以有无数个不重名的子命令。 210 | 211 | 子命令的前缀将被忽略。 212 | 213 | 对一个命令注册子命令,调用其对象的 `addSubcommand` ,将子命令对象传入即可。 214 | 215 | 你可以嵌套子命令,如: 216 | 217 | ```java 218 | new JKookCommand("eg") 219 | .addSubcommand( 220 | new JKookCommand("sub") 221 | .addSubcommand( 222 | new JKookCommand("anothersub") 223 | // subcommand code here 224 | ) 225 | ) 226 | // more command code 227 | ``` 228 | 229 | 调用其中的 `anothersub` 命令只需要执行 `/eg sub anothersub` 即可。 230 | 231 | 一个简单的子命令示例可以在本章的示例代码的第 2 处找到。 232 | 233 | ## 参数解析系统 234 | 235 | **本节的内容需要 JKook API 版本为 0.38+ 。** 236 | 237 | 我相信 `String[]` 作为存放参数的容器对于复杂的命令是很不友好的,因为对于命令中的标准数据类型需要你自行调用相关的方法进行解析。 238 | 239 | 所以我们在高版本 API 引入了参数解析系统。 240 | 241 | 此系统中的核心方法有如下三个: 242 | * `snw.jkook.command.JKookCommand#addArgument` 243 | * `snw.jkook.command.JKookCommand#addOptionalArgument` 244 | * `snw.jkook.command.CommandManager#registerArgumentParser` 245 | 246 | 一个简单的参数解析系统的使用示例可以在本章的示例代码的第 3 处找到。 247 | 248 | ### JKookCommand#addArgument 249 | 250 | 此方法用于向你的命令对象增加一个必选参数。 251 | 252 | 当执行命令前无法解析出参数的内容时,命令将被拒绝执行。 253 | 254 | 其方法签名如下: 255 | 256 | ```java 257 | public final class JKookCommand { 258 | public JKookCommand addArgument(Class cls) { 259 | // 具体实现已忽略 260 | } 261 | } 262 | ``` 263 | 264 | 要求传入一个参数的具体类型,若此类型不受支持将会抛出异常。 265 | 266 | **传入的类型不可以是 `java.lang.Object` 的 `Class` 对象。** 267 | 268 | ### JKookCommand#addOptionalArgument 269 | 270 | 此方法用于向你的命令对象增加一个可选参数。 271 | 272 | 当执行命令前无法解析出参数的内容时,将向命令执行器传入提供的默认值。 273 | 274 | 其方法签名如下: 275 | 276 | ```java 277 | public final class JKookCommand { 278 | public JKookCommand addOptionalArgument(Class cls, T defaultValue) { 279 | // 具体实现已忽略 280 | } 281 | } 282 | ``` 283 | 284 | 要求传入一个参数的具体类型以及一个与传入的类型所对应的对象作为默认值,若此类型不受支持将会抛出异常。 285 | 286 | **传入的类型不可以是 `java.lang.Object` 的 `Class` 对象。** 287 | 288 | ### CommandManager#registerArgumentParser 289 | 290 | 此方法用于注册一个参数解析器。 291 | 292 | **请一定要在注册命令前注册自定义的参数解析器!** 293 | 294 | 一个 JKook API 的实现默认提供以下类型的解析器: 295 | ```text 296 | int 及其包装器类型 java.lang.Integer 297 | double 及其包装器类型 java.lang.Double 298 | boolean 及其包装器类型 java.lang.Boolean 299 | String 300 | snw.jkook.entity.User 301 | snw.jkook.entity.TextChannel 302 | ``` 303 | 304 | 为什么没有 `float` 的? 305 | 306 | 可以用 `double` 类型替代。或者你可以自行注册一个。 307 | 308 | `User` 的解析器通过解析 [KMarkdown](https://developer.kookapp.cn/doc/kmarkdown) 中的 `(met)` 标签实现。它在 KOOK 客户端中的表现是 `@某人` 。 309 | 310 | `TextChannel` 的解析器通过解析 [KMarkdown](https://developer.kookapp.cn/doc/kmarkdown) 中的 `(chn)` 标签实现。它在 KOOK 客户端中的表现是 `#某频道` 。 311 | 312 | --- 313 | 314 | 本章我们讲解了 JKook API 的命令系统。 315 | 316 | 新的命令系统也是新版本 API 的亮点,希望新的命令系统能帮助你更优雅的写代码! 317 | -------------------------------------------------------------------------------- /ch_6/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_6/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_6/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.command.JKookCommand; 4 | import snw.jkook.entity.User; 5 | import snw.jkook.message.component.MarkdownComponent; 6 | import snw.jkook.message.component.TextComponent; 7 | import snw.jkook.plugin.BasePlugin; 8 | 9 | public class Main extends BasePlugin { 10 | 11 | @Override 12 | public void onEnable() { 13 | /* 1 */ 14 | // "eg" is the root name of the command. 15 | // In this example, the command prefix is "/" (default value) 16 | // So you can execute this command by sending a message which the content is "/eg" in KOOK. 17 | new JKookCommand("eg") 18 | // You can add another string as the new prefix of this command. 19 | // This method can be called for many times. 20 | // The only restriction is that the provided string cannot have space characters. 21 | .addPrefix(".") 22 | // You can add an alias for this command. 23 | // This method can be called for many times. 24 | // The restriction is the same as addPrefix method. (No space character in the provided string) 25 | .addAlias("example") 26 | // Actually, the /help command is provided by JKook API implementations. 27 | // So it's possible to make the description never be shown for some API implementations. 28 | // This rule is also applicable to the help content of the commands. 29 | .setDescription("The description will be shown in the result of /help command") 30 | .setHelpContent("The help content will be shown in the result of '/help eg' command") 31 | // This method is the only way to set the executor in JKook API 0.37 and before. 32 | // The sender type is CommandSender, so it might be a User or ConsoleCommandSender. 33 | // So you need to write (if-else + instanceof) statements to know the sender. 34 | // For users, you need to cast them into User manually. 35 | // so we added executeUser and executeConsole to prevent this problem. 36 | // Tips: The CommandExecutor won't be executed if the command have a UserCommandExecutor 37 | // and the executor is User, or the command have a ConsoleCommandExecutor 38 | // and the sender is ConsoleCommandSender. 39 | // This is just for the backwards compatibility. 40 | // Actually, it is deprecated. 41 | .setExecutor((sender, arguments, message) -> { 42 | // the commands can be invoked by using CommandManager#executeCommand. 43 | // That method will invoke the command with sender and arguments, but no message object provided. 44 | // So if you don't check the message, a NullPointerException will be thrown. 45 | if (sender instanceof User && message != null) { 46 | message.reply(new MarkdownComponent("Hi!")); 47 | } 48 | }) 49 | // The better way to set the command executor for users since JKook 0.38 50 | .executesUser((sender, arguments, message) -> { 51 | if (message != null) { 52 | message.reply(new TextComponent("Hi!")); 53 | } 54 | }) 55 | .executesConsole((sender, arguments) -> { 56 | // console command executors cannot be triggered through KOOK application. 57 | // so there is no message object available. 58 | this.getLogger().info("Hi!"); 59 | }) 60 | .register(this); // DO NOT FORGET TO REGISTER COMMAND AT THE END OF CALL CHAIN! 61 | // .register() // If you are using JKook API 0.37 (or before), you don't need to provide 62 | // Plugin instance to the register method. 63 | // 64 | // In JKook API 0.37 and before, we don't need a Plugin instance for registering the command 65 | // But it is not good for the command management (e.g. unregister command when disabling plugins) 66 | // So I made a breaking change in API 0.38, plugin developers need to provide a Plugin instance 67 | // to register the command since that commit 68 | // 69 | // WARNING: THE ATTRIBUTES OF THE COMMAND CANNOT BE EDITED AFTER IT REGISTERED. 70 | 71 | /* 2 */ 72 | new JKookCommand("greet") 73 | // You can add many subcommands by using addSubcommand method. 74 | .addSubcommand( 75 | new JKookCommand("name") 76 | .executesUser((sender, arguments, message) -> { 77 | if (message == null) { 78 | return; // Invoked by using CommandManager#executeCommand 79 | // You can halt the command execution if you want 80 | } 81 | if (arguments.length == 0) { 82 | message.reply(new MarkdownComponent("No name?")); 83 | return; // there is no argument available, we should return 84 | } 85 | // In JKook API 0.37 (and before), the result of arguments[0] is a String. 86 | // But in JKook API 0.38+, the result is Object. 87 | // Why? Because API 0.38 added the argument parser system, 88 | // You can specify the argument type by using JKookCommand#addArgument. 89 | message.reply(new MarkdownComponent("Hi! " + arguments[0])); 90 | }) 91 | ) 92 | .register(this); 93 | 94 | /* 3 */ 95 | // The following command can only be registered on JKook API 0.38+. 96 | new JKookCommand("arg") 97 | // You can specify the command argument type by using addArgument method. 98 | // Java standard data types are supported by default. 99 | .addArgument(String.class) 100 | .addArgument(int.class) 101 | // Optional argument is supported. 102 | // For example, in this command, if you execute "/arg SNWCreations 159", 103 | // the value of this argument is "play" 104 | // But if you execute "/arg SNWCreations 60 Sleep" 105 | // the value of this argument is "sleep" 106 | .addOptionalArgument(String.class, "play") 107 | // Also, you can specify custom type. 108 | // But the parser of the type that you provided is required before registering this command. 109 | .executesUser((sender, arguments, message) -> { 110 | // I'm sure arguments.length is 3. 111 | if (message == null) { 112 | return; 113 | } 114 | // I'm sure the following cast will be succeeded 115 | String name = (String) arguments[0]; 116 | int money = (int) arguments[1]; 117 | String action = (String) arguments[2]; 118 | message.reply(new MarkdownComponent( 119 | String.format("%s has %s CNY.\nHis next action is %s.", name, money, action) 120 | )); 121 | }) 122 | .register(this); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /ch_6/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_7/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 7 2 | 3 | 在这个世界上,每一分,每一秒,都在发生一些事情,传递这些事情"发生了"这个信息的东西我们称之为事件。 4 | 5 | 本章我们讲解 JKook API 的事件体系。以及如何监听它们。 6 | 7 | ## 运行一次示例 8 | 9 | 如果你读过前两章,我不多说了,自己编译一下示例代码,跑一下。 10 | 11 | 这个示例插件的效果是: 当你在机器人能访问的频道中发送了消息后,机器人会回复你 `Hello world!` 。 12 | 13 | ## 总览包结构 14 | 15 | 所有由 JKook API 提供的事件均放在了 `snw.jkook.event` 包下。 16 | 17 | 先总览包结构。 18 | 19 | ```text 20 | snw.jkook.event 21 | | Event 22 | | EventHandler 23 | | EventManager 24 | | Listener 25 | | TimedEvent (不存在于 API 0.37) 26 | | 27 | +---channel 28 | | ChannelCreateEvent 29 | | ChannelDeleteEvent 30 | | ChannelEvent 31 | | ChannelInfoUpdateEvent 32 | | ChannelMessageDeleteEvent 33 | | ChannelMessageEvent 34 | | ChannelMessagePinEvent 35 | | ChannelMessageUnpinEvent 36 | | ChannelMessageUpdateEvent 37 | | 38 | +---guild 39 | | GuildAddEmojiEvent 40 | | GuildBanUserEvent 41 | | GuildDeleteEvent 42 | | GuildEvent 43 | | GuildInfoUpdateEvent 44 | | GuildRemoveEmojiEvent 45 | | GuildUnbanUserEvent 46 | | GuildUpdateEmojiEvent 47 | | GuildUserNickNameUpdateEvent 48 | | 49 | +---item 50 | | ItemConsumedEvent 51 | | ItemEvent 52 | | 53 | +---pm 54 | | PrivateMessageDeleteEvent 55 | | PrivateMessageEvent 56 | | PrivateMessageReceivedEvent 57 | | PrivateMessageUpdateEvent 58 | | 59 | +---role 60 | | RoleCreateEvent 61 | | RoleDeleteEvent 62 | | RoleEvent 63 | | RoleInfoUpdateEvent 64 | | 65 | \---user 66 | UserAddReactionEvent 67 | UserClickButtonEvent 68 | UserEvent 69 | UserInfoUpdateEvent 70 | UserJoinGuildEvent 71 | UserJoinVoiceChannelEvent 72 | UserLeaveGuildEvent 73 | UserLeaveVoiceChannelEvent 74 | UserOfflineEvent 75 | UserOnlineEvent 76 | UserRemoveReactionEvent 77 | ``` 78 | 79 | ## 重点类详解 80 | 81 | ### Event 82 | 83 | 其完整限定名为 `snw.jkook.event.Event` 。 84 | 85 | 所有的 JKook 事件均继承此类。 86 | 87 | #### Event 与 TimedEvent 88 | 89 | `TimedEvent` 是在更新的 JKook API 中加入的,是 `Event` 的子类。 90 | 91 | 我们将原本存放在 `Event` 对象中的时间戳转移到了 `TimedEvent` ,这么做可以方便开发者自定义事件。 92 | * 自定义事件将在下文讲解。 93 | 94 | _然而,在 API 0.37 没有 `TimedEvent` 。所以,在 API 0.37 中,自定义事件的构造方法需要给 `Event` 的构造方法传递一个数字作为时间戳,如果你的事件不需要时间戳,请放心地传 -1 ,只要让你的使用者也知道就好。_ 95 | 96 | ### EventManager 97 | 98 | 其完整限定名为 `snw.jkook.event.EventManager` 。 99 | 100 | 你可以通过此接口的实现注册事件监听器,以及发布一个事件。 101 | 102 | ### Listener 103 | 104 | 其完整限定名为 `snw.jkook.event.Listener` 。 105 | 106 | 它是一个接口,你需要在写了事件监听器的类中 `implements` 此接口。 107 | 108 | 此接口没有任何需要你实现的方法,它自己本就没有定义任何方法。 109 | 110 | 它的作用仅仅是标记,以及将你的监听器类与 `Object` 区分。 111 | 112 | 因此,像这样的空类是可以写的: 113 | ```java 114 | public class MyListener implements Listener { 115 | } 116 | ``` 117 | 118 | 但它无意义。 119 | 120 | ### EventHandler 121 | 122 | 其完整限定名为 `snw.jkook.event.EventHandler` 。 123 | 124 | 它是一个注解,用于标记你的方法是一个用于处理事件的方法。 125 | 126 | 声明一个事件监听器可以这么写: 127 | ```java 128 | public class MyListener implements Listener { 129 | @EventHandler 130 | public void onEvent(XXXEvent event) { // 将 XXXEvent 替换为一种具体的事件 131 | // code here 132 | } 133 | } 134 | ``` 135 | 136 | 写给 Bukkit 开发者: JKook API 提供的所有事件均无法取消。不同于 Bukkit 优先于 Minecraft Server 所以可以修改事件,KOOK 的事件是在发生后才推送给机器人。也正因为这个,我们认为没有必要编写监听器优先级系统,故 `EventHandler` 没有 `priority` 属性。 137 | 138 | ## 监听事件 139 | 140 | 这才是重头戏。 141 | 142 | 有这么多的事件,那我们怎么知道它们发生了呢? 143 | 144 | 上文其实已经提了: 145 | ```java 146 | public class MyListener implements Listener { 147 | @EventHandler 148 | public void onEvent(XXXEvent event) { // 将 XXXEvent 替换为一种具体的事件 149 | // code here 150 | } 151 | } 152 | ``` 153 | 154 | 但是只有这个还不够,我们还需要用 `EventManager#registerHandlers` 方法注册监听器。 155 | * **写了监听器类一定要注册!** 156 | 157 | 最常见的注册方法莫过于: 158 | 159 | ```java 160 | public class MyPlugin extends BasePlugin { 161 | 162 | @Override 163 | public void onEnable() { 164 | getCore().getEventManager().registerHandlers(this, new MyListener()); 165 | } 166 | } 167 | ``` 168 | 169 | **注意,监听的事件类型不能是抽象的(即 `abstract` 的)。** 170 | 171 | 因为篇幅原因,具体的事件意义请自行查阅 JKook API 文档或 [KOOK 开发者文档](https://developer.kookapp.cn/doc/event/event-introduction)。 172 | 173 | ## 自定义事件 174 | 175 | 我们支持自定义事件! 176 | 177 | 若你的插件中有一些有价值的事件,不妨通过我们的事件系统分享出来! 178 | 179 | 写一个直接继承 `Event` 的类即可。 180 | 181 | 然后在你的事件发生时,`new` 一个事件对象,然后通过 `EventManager#callEvent` 方法发布出去! 182 | 183 | **注意:`callEvent` 方法是同步的。** 184 | 这意味着这个方法会在所有监听了你提交的事件的代码返回之后才返回,如果你只需要发布一个事件,而不需要关心后续,可以考虑用任务调度器发布事件,以提升代码性能。任务调度器的使用将在下章讲解。 185 | 186 | --- 187 | 188 | 本章我们讲解了 JKook API 事件系统的结构。 189 | 190 | 其实并没有什么深奥的,只是具体的事件有何内容需要你自行探索。 191 | -------------------------------------------------------------------------------- /ch_7/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_7/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_7/code/src/main/java/snw/jkook/example/EventListener.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.event.EventHandler; 4 | import snw.jkook.event.Listener; 5 | import snw.jkook.event.channel.ChannelMessageEvent; 6 | 7 | public class EventListener implements Listener { 8 | @EventHandler 9 | public void onMessage(ChannelMessageEvent event) { 10 | event.getMessage().reply("Hello world!"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch_7/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.plugin.BasePlugin; 4 | 5 | public class Main extends BasePlugin { 6 | 7 | @Override 8 | public void onEnable() { 9 | this.getCore().getEventManager().registerHandlers(this, new EventListener()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ch_7/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_8/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 8 2 | 3 | 本章我们讲解 JKook API 中的任务调度系统。 4 | 5 | ## 运行一次示例 6 | 7 | 是的,本章有示例代码。 8 | 9 | 编译,运行,看看效果? 10 | 11 | 效果是: 在你发送 `/notice` 命令后,每经过 1 分钟,机器人会向你发送一条私信,内容是 `I'm the task in the scheduler!` 。 12 | 13 | ## 内容详情 14 | 15 | JKook API 的任务调度系统位于 `snw.jkook.scheduler` 包下。 16 | 17 | 只有三个类,所以就不写包结构了。 18 | 19 | 它们分别是 `Scheduler`,`Task` 和 `JKookRunnable` 。 20 | 21 | ## Scheduler 22 | 23 | 其完整限定名为 `snw.jkook.scheduler.Scheduler` 。 24 | 25 | 这是整个 JKook 任务调度系统的核心。表示一个任务调度器。 26 | 27 | 所有提交给调度器的任务会统一在一个线程池中运行。 28 | 29 | 任务调度器提供如下方法: 30 | * `runTask` - 让任务在线程池中立刻运行 31 | * `runTaskLater` - 让任务在一定时间后才运行 32 | * `runTaskTimer` - 让任务反复以一段时间为间隔运行,直到其被取消 33 | * `isScheduled` - 接受一个任务 ID(类型为 `int`),然后检查是否有一个任务与之对应,有则返回 `true` ,无则返回 `false` 34 | * `cancelTask` - 接受一个任务 ID(类型为 `int`),然后取消与其对应的任务 35 | * `cancelTasks` - 取消由一个插件预定的所有任务 36 | 37 | 所有发布任务的方法都需要 `Plugin` 实例。 38 | * 在 API 0.37 中,`runTask` 方法不需要 `Plugin` 实例,但是在高版本中需要,还请注意。 39 | 40 | **这里提供的所有 `runTaskXXX` 方法的时间单位为毫秒。下文将要提及的 `JKookRunnable` 同理。** 41 | 42 | ## Task 43 | 44 | 其完整限定名为 `snw.jkook.scheduler.Task` 。 45 | 46 | 表示一个任务。 47 | 48 | 它提供如下方法: 49 | * `getPlugin` - 获取预定了它的插件 50 | * `cancel` - 取消它,但是当其 `isCancelled` 方法返回 `true` 时,抛出 `IllegalStateException` 异常。 51 | * `isCancelled` - 当此任务已经被取消时返回 `true` 52 | * `isExecuted` - 当此任务已被执行过一次时返回 `true` 53 | * `getTaskId` - 获取此任务的 ID 54 | 55 | ## JKookRunnable 56 | 57 | 写给 Bukkit 开发者: 这个名字很眼熟?是的,它复刻了 `BukkitRunnable` ! 58 | 59 | 其完整限定名为 `snw.jkook.scheduler.JKookRunnable` 。 60 | 61 | 它提供了一个更便于向任务调度器发布任务的方法。 62 | 63 | 它继承自 `java.lang.Runnable` ,但是其 `run` 方法需要你自行实现————这也本就是你使用它时应该做的。 64 | 65 | 它一比一复刻了 `Scheduler` 接口中的部分方法。 66 | 67 | 它提供如下方法: 68 | * `runTask` 69 | * `runTaskLater` 70 | * `runTaskTimer` 71 | 72 | 和任务调度器一样,每一个方法都需要 `Plugin` 实例。 73 | 74 | 它可以取消自己。使用 `cancel` 方法即可。因此,即使这个任务使用 `runTaskTimer` 方法发布,在满足某种条件时你可以手动调用 `cancel` 方法。 75 | 76 | -------------------------------------------------------------------------------- /ch_8/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_8/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_8/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.command.JKookCommand; 4 | import snw.jkook.message.component.MarkdownComponent; 5 | import snw.jkook.plugin.BasePlugin; 6 | import snw.jkook.scheduler.JKookRunnable; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class Main extends BasePlugin { 11 | 12 | @Override 13 | public void onEnable() { 14 | new JKookCommand("notice") 15 | .executesUser((sender, arguments, message) -> { 16 | new JKookRunnable() { 17 | @Override 18 | public void run() { 19 | sender.sendPrivateMessage(new MarkdownComponent("I'm the task from the scheduler!")); 20 | } 21 | }.runTaskTimer(this, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(1)); 22 | }) 23 | .register(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ch_8/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | -------------------------------------------------------------------------------- /ch_9/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 9 2 | 3 | _本章可以选择性阅读,你可以直接跳到下一章。_ 4 | 5 | --- 6 | 7 | 首先,感谢你读到了这里。 8 | 9 | 在前几章中,我们就算是已经讲解了 95% 的 JKook API 内容了。 10 | 11 | 而本章,我们会讲解一些进阶内容。 12 | 13 | ## Unsafe 14 | 15 | 其完全限定名为 `snw.jkook.Unsafe` 。 16 | 17 | 这个接口的实现提供了一些不安全的操作,虽然不安全,但是用好它们可以帮助你编码。 18 | 19 | 其实 `Unsafe` 我们早在 [CustomEmoji 简述 - 第 4 章](../ch_4/README.md#CustomEmoji) 中就有提到了。 20 | 21 | 那时,我们用了 `Unsafe#getCustomEmoji` 方法,所以在这里就不再讲解它了,需要的可以去上面的链接处看看。 22 | 23 | 而现在,让我们再看这个接口。 24 | 25 | 其实这个接口的内容不多,也就是提供了几个构造实体类的方法。 26 | 27 | 它们分别是: 28 | * `getTextChannelMessage` - 通过提供的 ID 构造一个 `TextChannelMessage` 并返回 29 | * `getPrivateMessage` - 通过提供的 ID 构造一个 `PrivateMessage` 并返回 30 | * `getEmoji` - 通过提供的 ID 构造一个 `CustomEmoji` 并返回 31 | * `getGame` - 通过提供的 ID 构造一个 `Game` 并返回 32 | 33 | **注意:** 34 | * 以上列出的方法的返回结果中,除了 `getId` 方法,所有以 `get` 开头的方法均是不可用的,它们不能给出正确的数据。 35 | * 以上列出的方法均不会检查它们得到的参数。 36 | 37 | 一些修改其属性的方法也许可用。 38 | 39 | 我们推荐你在使用此接口时参照 JKook API 实现的相应源代码看。 40 | 41 | ## 配置系统中的序列化 42 | 43 | 这也许本应该在第 2 章就写了。 44 | 45 | 但是因为这是进阶内容,所以放在了这章。 46 | 47 | 本章的示例代码就是为此准备的。你可以先运行一次,看看它生成的 `config.yml` ,然后在阅读下文的过程中阅读它的源代码。 48 | 49 | 这里讲解 `snw.jkook.config.file.serialization` 包。 50 | 51 | 这个包提供了一个将你的数据结构更方便地序列化的途径。 52 | 53 | 我们将依次讲解以下类: 54 | * ConfigurationSerializable 55 | * ConfigurationSerialization 56 | * SerializableAs 57 | * DelegateDeserialization 58 | 59 | ### ConfigurationSerializable 60 | 61 | 其完整限定名为 `snw.jkook.config.file.serialization.ConfigurationSerializable` 。 62 | 63 | 实现它的类将被 JKook 序列化系统支持。从而使其可以在保存配置数据时按序列化系统的逻辑序列化,也可以正确地反序列化。 64 | 65 | 实现它分三步: 66 | 1. 创建一个类(`class`) 67 | 2. 使其实现 `ConfigurationSerializable` 接口及其声明的 `serialize` 方法。 68 | 3. 使这个类满足以下三个条件中的一个: 69 | * 有一个接受 `Map` 的构造方法 70 | * 有一个静态的(`static`的),接受 `Map` 的,名字为 `deserialize` 的方法。 71 | * 有一个静态的(`static`的),接受 `Map` 的,名字为 `valueOf` 的方法。 72 | 73 | ### ConfigurationSerialization 74 | 75 | 其完整限定名为 `snw.jkook.config.file.serialization.ConfigurationSerialization` 。 76 | 77 | 这个类用于注册/注销 `ConfigurationSerializable` 接口的实现。 78 | 79 | 在按照上文列举的三步创建了一个 `ConfigurationSerializable` 的实现后,你还需要让 JKook API 知道。 80 | 81 | 直接通过向 `ConfigurationSerialization#registerClass` 方法传递你的 `ConfigurationSerializable` 实现类的 `Class` 对象即可。 82 | 83 | 就像这样: 84 | ```java 85 | ConfigurationSerialization.registerClass(DataObj.class); 86 | ``` 87 | 88 | 在这个示例中,`DataObj` 类是一个 `ConfigurationSerializable` 的实现。 89 | 90 | ### DelegateDeserialization 91 | 92 | 其完整限定名为 `snw.jkook.config.file.serialization.DelegateDeserialization` 。 93 | 94 | 这是一个注解,用于在一个 `ConfigurationSerializable` 接口的实现类上指定另一个 `ConfigurationSerializable` 接口的实现类,让被指定的类完成自己的反序列化。 95 | 96 | 比如在示例代码中,`AnotherDataObj` 类就指定了 `DataObj` 类完成自己的反序列化。 97 | 98 | 而 `DataObj` 类的反序列化方法是这么写的: 99 | ```java 100 | public static ConfigurationSerializable deserialize(Map data) { 101 | String name = data.get("name").toString(); 102 | if (name.startsWith("another")) { // 专门用于匹配 AnotherDataObj 的条件 103 | return new AnotherDataObj(name); // 如果成立,构造 AnotherDataObj 的实例,而不是 DataObj 104 | } 105 | return new DataObj(name); 106 | } 107 | ``` 108 | 109 | 这个注解的好处是可以避免写大量的 `deserialize` 方法,只需要按照特定条件去检查即可。 110 | 111 | 但过量使用容易导致被指定的类的反序列化方法太过复杂。 112 | 113 | 而且,严格意义上,这个注解使代码违背了 "单一职责" 原则。 114 | * "单一职责" 原则即一段代码只做一种事情。 115 | 116 | 按需使用。 117 | 118 | ### SerializableAs 119 | 120 | 其完整限定名为 `snw.jkook.config.file.serialization.SerializableAs` 。 121 | 122 | 这也是一个注解。 123 | 124 | 它接受一个 `String` 作为它的 `value` ,表示在序列化时使用给定的名称作为它的标识。 125 | 126 | 举个例子: 127 | ```yml 128 | data: # 数据的名称 129 | ==: ClassName # SerializableAs 决定这里的值 130 | foo: bar # 具体的数据 131 | ``` 132 | 133 | 如果要使用这个注解,提供的名称必须独一无二,比如你可以在具体的名称前加上插件的名称(形如 `MyPluginThing`的形式)。 134 | 135 | ## 一次性 Listener 136 | 137 | 在前面的 [第 7 章](../ch_7/README.md) 中,关于 `EventManager` 其实有一个方法没讲。 138 | 139 | `EventManager#unregisterHandlers` 。 140 | 141 | 这个方法会使传入的 `Listener` 实例不再收到事件。 142 | 143 | 看个例子: 144 | ```java 145 | public class ExecuteOnceListener implements Listener { 146 | @EventHandler 147 | public void onMessage(ChannelMessageEvent event) { 148 | Plugin plugin; // 你的插件的实例 149 | event.getMessage().reply("Execute once!"); 150 | plugin.getCore().getEventManager().unregisterHandlers(this); 151 | } 152 | } 153 | ``` 154 | 155 | 这就是一个最简单的一次性 `Listener` 的实现。 156 | 157 | 当然,你也可以让 `Listener` 在一个特定条件时失效,发挥你的想象力! 158 | -------------------------------------------------------------------------------- /ch_9/code/.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /ch_9/code/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snw.jkook 8 | example 9 | 1.0.0 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | jitpack.io 19 | https://jitpack.io 20 | 21 | 22 | 23 | 24 | 25 | com.github.SNWCreations 26 | JKook 27 | 0.49.0 28 | provided 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ch_9/code/src/main/java/snw/jkook/example/AnotherDataObj.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.config.serialization.ConfigurationSerializable; 4 | import snw.jkook.config.serialization.DelegateDeserialization; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @DelegateDeserialization(value = DataObj.class) 10 | public class AnotherDataObj implements ConfigurationSerializable { 11 | private final String name; 12 | 13 | public AnotherDataObj(String name) { 14 | this.name = name; 15 | } 16 | 17 | @Override 18 | public Map serialize() { 19 | Map result = new HashMap<>(); 20 | result.put("name", name); 21 | return result; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return "AnotherDataObj{" + 27 | "name='" + name + '\'' + 28 | '}'; 29 | } 30 | 31 | // will be called by the JKook's serialization system 32 | @SuppressWarnings("unused") 33 | public static AnotherDataObj deserialize(Map data) { 34 | return new AnotherDataObj(data.get("name").toString()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ch_9/code/src/main/java/snw/jkook/example/DataObj.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.config.serialization.ConfigurationSerializable; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class DataObj implements ConfigurationSerializable { 9 | private final String name; 10 | 11 | public DataObj(String name) { 12 | this.name = name; 13 | } 14 | 15 | @Override 16 | public Map serialize() { 17 | Map result = new HashMap<>(); 18 | result.put("name", name); 19 | return result; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "DataObj{" + 25 | "name='" + name + '\'' + 26 | '}'; 27 | } 28 | 29 | // will be called by the JKook's serialization system 30 | @SuppressWarnings("unused") // actually it will be used 31 | public static ConfigurationSerializable deserialize(Map data) { 32 | String name = data.get("name").toString(); 33 | if (name.startsWith("another")) { 34 | return new AnotherDataObj(name); 35 | } 36 | return new DataObj(name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ch_9/code/src/main/java/snw/jkook/example/Main.java: -------------------------------------------------------------------------------- 1 | package snw.jkook.example; 2 | 3 | import snw.jkook.config.serialization.ConfigurationSerialization; 4 | import snw.jkook.plugin.BasePlugin; 5 | 6 | public class Main extends BasePlugin { 7 | 8 | @Override 9 | public void onLoad() { 10 | ConfigurationSerialization.registerClass(DataObj.class); 11 | saveDefaultConfig(); 12 | } 13 | 14 | @Override 15 | public void onEnable() { 16 | Object data = getConfig().get("data"); 17 | Object data2 = getConfig().get("data2"); 18 | getLogger().info("The deserialized data: {}", data); 19 | getLogger().info("The deserialized data2: {}", data2); 20 | if (data == null || data2 == null){ 21 | getLogger().info("No data? Creating new objects, it will be saved on disable"); 22 | getConfig().set("data", new DataObj("theName")); 23 | getConfig().set("data2", new AnotherDataObj("anotherObj")); 24 | } 25 | } 26 | 27 | @Override 28 | public void onDisable() { 29 | saveConfig(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ch_9/code/src/main/resources/config.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SNWCreations/JKookTutorial/a90b9c1e4e820b45aa9c6d56b02e3570e65e42df/ch_9/code/src/main/resources/config.yml -------------------------------------------------------------------------------- /ch_9/code/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ExampleBot 2 | version: 0.1.0 3 | api-version: 0.49.0 4 | authors: ["SNWCreations"] 5 | main: snw.jkook.example.Main 6 | --------------------------------------------------------------------------------