Author SHA1 Message Date
RTAkland bc28a4fefc chore: bump rob version 2024-10-23 20:31:45 +08:00
RTAkland f0d92d8fe7 feat: add http dog command 2024-10-23 18:40:47 +08:00
RTAkland 184378c907 chore: move http cat command to misc folder 2024-10-23 15:15:26 +08:00
RTAkland fc864529b5 chore: use local image file instaed of api 2024-10-23 15:12:14 +08:00
RTAkland 4e0cb17f4e feat: add custom http cat 2024-10-23 15:07:43 +08:00
RTAkland 2ce9d3dc8f feat: add random http cat 2024-10-23 13:45:44 +08:00
RTAkland f48b2ea8b3 feat: support ping srv record mc server 2024-10-23 13:32:46 +08:00
RTAkland 57d0737f21 feat: add ip geo 2024-10-23 13:29:56 +08:00
RTAkland 0d73a29d34 feat: add qq image bed 2024-10-22 20:31:09 +08:00
RTAkland 7511dfd37f chore: bump rob version 2024-10-22 18:24:49 +08:00
RTAkland 3eabe254af fix 2024-10-21 20:27:14 +08:00
RTAkland 97cd846b53 feat: add srv record lookup 2024-10-21 17:58:11 +08:00
RTAkland d8b4f5003b chore: update ts language color 2024-10-21 13:45:04 +08:00
RTAkland ac96431552 chore: format code 2024-10-19 19:29:53 +08:00
RTAkland 8fd92b8fed chore: use user role instead of admin list 2024-10-19 19:29:03 +08:00
RTAkland 9381ef8637 chore: bump ronebot version 2024-10-19 18:46:08 +08:00
RTAkland b64f3580d0 chore: bump ronebot 2024-10-19 14:57:57 +08:00
RTAkland 3fba603b17 chore: at sender 2024-10-19 10:43:10 +08:00
RTAkland 492f36d6a5 nothing 2024-10-19 10:06:07 +08:00
RTAkland 10a61fbee7 chore: remove bot limit 2024-10-18 18:23:18 +08:00
RTAkland b0d295d97f feat: add max player tip 2024-10-18 17:38:04 +08:00
RTAkland f5d6a3acb7 chore: remove dnsjava 2024-10-18 13:22:23 +08:00
RTAkland f5cc673836 chore: move to version catalog 2024-10-18 13:19:49 +08:00
RTAkland 838d95391e feat: improve mcbot command 2024-10-18 11:38:45 +08:00
RTAkland 8999e8912a chore: migrate to version catalog 2024-10-18 01:11:59 +08:00
RTAkland 5d3a31dd28 chore: format code 2024-10-18 01:03:38 +08:00
RTAkland 28cf80c271 feat: add mc bot command 2024-10-18 00:56:37 +08:00
RTAkland b3c3e230bf feat: support reverse gif using reply 2024-10-17 22:16:27 +08:00
RTAkland 1b7f09732f feat: add today dont eat/drink command 2024-10-17 21:36:45 +08:00
RTAkland e5b8b42d7a chore: bump rob 2024-10-17 21:26:40 +08:00
RTAkland f22934c325 fix static image not make short url 2024-10-17 15:54:59 +08:00
RTAkland 9a7d3af844 chore: rename 2024-10-17 15:32:12 +08:00
RTAkland bbb552dc14 chore: format code 2024-10-17 13:02:19 +08:00
RTAkland 2ac27d29e7 feat: support mysql and postgresql database 2024-10-17 12:51:20 +08:00
RTAkland 087d55ba28 chore: remove unused import 2024-10-15 19:08:15 +08:00
RTAkland fbcdbe301a feat: add shuffled echo command 2024-10-15 19:07:18 +08:00
RTAkland 6b4ef38443 chore: format code 2024-10-15 18:58:12 +08:00
RTAkland c4e6f9e348 chore: format number 2024-10-15 15:49:56 +08:00
RTAkland a59a544792 fix: fix cover 2024-10-15 15:47:48 +08:00
RTAkland c2e1ec91b5 feat: support parse youtube video 2024-10-15 15:40:33 +08:00
RTAkland 88fd259712 chore: remove message reaction 2024-10-15 15:16:52 +08:00
RTAkland ae3eae4e71 chore: add command usage tip 2024-10-15 13:59:11 +08:00
RTAkland ac88dcc296 chore: use short api 2024-10-15 11:13:54 +08:00
RTAkland 7f3b1fd1b5 feat: auto reaction admin 2024-10-14 23:17:53 +08:00
RTAkland e375f20e9c feat: support gen short link and create pastebin using reply 2024-10-14 22:41:01 +08:00
RTAkland bfbeee2880 chore: add fail fallback exception catch 2024-10-14 19:24:45 +08:00
RTAkland c4d8c0eaf0 feat: add pastebin generate action log 2024-10-14 17:43:26 +08:00
RTAkland f96f7b7055 fix: fix bili video parse error and add pastebin command 2024-10-14 17:40:04 +08:00
RTAkland 6f32323fec feat: add gravatar command 2024-10-14 17:26:59 +08:00
RTAkland 73b417775a feat: add rcon command 2024-10-13 14:16:00 +08:00
RTAkland c9d2ddeff7 fix: fix wrong avatar and image scale 2024-10-13 12:20:37 +08:00
RTAkland f0e6f31faf chore: format code 2024-10-13 11:58:31 +08:00
RTAkland a78a365ebd feat: support parse douyin video 2024-10-13 11:57:56 +08:00
RTAkland d036c2bdba chore: add image expired exception catch 2024-10-13 10:55:08 +08:00
RTAkland c831a76850 chore: split image bed command to single file 2024-10-13 10:52:37 +08:00
RTAkland ec6e186284 fix: fix sign out of image 2024-10-12 21:04:11 +08:00
RTAkland 95355d92fe feat: support generate ascii art with reply 2024-10-12 20:59:31 +08:00
RTAkland ff65333a8f feat: support parse bilibili user 2024-10-12 18:35:16 +08:00
RTAkland 499eab9549 docs: update docs 2024-10-12 17:29:09 +08:00
RTAkland 242ce3e60e docs: update docs 2024-10-12 17:28:05 +08:00
RTAkland 83e3404665 chore: format code 2024-10-12 15:23:21 +08:00
RTAkland 6bc77fb4ac chore: add command description for mc login command 2024-10-11 20:50:26 +08:00
RTAkland 217364ffb8 feat: add mclogin command 2024-10-11 20:49:32 +08:00
RTAkland 2ac7d3b20b fix: fix message target 2024-10-11 19:36:22 +08:00
RTAkland 5ac05f052a feat: add reaction other's message command 2024-10-11 19:28:22 +08:00
RTAkland 1b1a579f23 chore: add command description annotation 2024-10-11 19:24:33 +08:00
RTAkland 1aab83ae51 feat: complete today's eat and drink commands 2024-10-11 19:10:06 +08:00
RTAkland c428b84e10 chore: nothing 2024-10-11 19:03:42 +08:00
RTAkland e01a98d254 chore: nothing 2024-10-11 19:03:21 +08:00
RTAkland f783f8b4ec feat: add new command 2024-10-11 17:13:20 +08:00
RTAkland 1c3f582c86 feat: add black list manager to manage group who kicked the bot 2024-10-11 17:12:50 +08:00
RTAkland a0b74d3df4 feat: add niuzi manager to manage user's niuzi length and bank balance 2024-10-11 14:00:00 +08:00
RTAkland ccb72a7277 feat: add ascii art upload to image bed 2024-10-11 13:47:26 +08:00
RTAkland 7fda4dc333 chore: add api rtast key 2024-10-11 11:08:18 +08:00
RTAkland 68b7c60a4d chore: set r2 mime type 2024-10-10 19:47:27 +08:00
RTAkland e0e89d1ecb chore: add slash 2024-10-10 18:20:32 +08:00
RTAkland 6da5553d1f docs: update docs -> update jar size 2024-10-10 18:07:45 +08:00
RTAkland 90225ae147 feat: add idiom explain and add mcping latency 2024-10-10 16:03:51 +08:00
RTAkland 1c00093560 fix: fix wikitext parse 2024-10-10 14:59:09 +08:00
RTAkland 7b8120dbc5 chore: rename dep 2024-10-10 11:58:48 +08:00
RTAkland af6f580583 feat: add minecraft wiki content query 2024-10-10 11:57:53 +08:00
RTAkland ac7910d20c docs: update docs 2024-10-10 10:55:16 +08:00
RTAkland b8568c00e7 chore: format code 2024-10-10 10:42:15 +08:00
RTAkland 52c791d3ed feat: add gaokao days remain command 2024-10-09 23:28:57 +08:00
RTAkland f34e12b3d2 feat: add more commands about minecraft 2024-10-09 23:14:41 +08:00
RTAkland c280c02a4e chore: change default r2 public url 2024-10-09 21:05:04 +08:00
RTAkland 829f858840 feat: use cloudflare r2 oss instead of github repository 2024-10-09 20:52:24 +08:00
RTAkland fdad0054ef chore: use cloudflare kv instead of vercel kv 2024-10-09 18:35:28 +08:00
RTAkland cd69d9a18e feat: support get multi image get 2024-10-09 14:55:03 +08:00
RTAkland 44c622190d feat: add short url and origin url 2024-10-09 13:27:01 +08:00
RTAkland f9e6bac23d chore: add catch exception 2024-10-09 13:17:13 +08:00
RTAkland 06a385db1a feat: add image bed command 2024-10-09 13:06:43 +08:00
RTAkland b660b504fd chore: add less language color 2024-10-08 22:16:03 +08:00
RTAkland 8504533659 feat: add image bed api 2024-10-08 20:48:02 +08:00
RTAkland 92bf62347c chore: format size 2024-10-08 20:47:49 +08:00
RTAkland 8ae4ce7870 feat: add image bed api 2024-10-08 20:45:56 +08:00
RTAkland 54354e0a52 chore: add vb6.0 language color 2024-10-08 18:57:46 +08:00
RTAkland a80cbdcb1a chore: format code 2024-10-08 17:19:54 +08:00
RTAkland 9ff4f03b24 chore: use ext property instead of func 2024-10-08 17:06:01 +08:00
RTAkland 95a6abbfb3 chore: use proxy 2024-10-08 17:03:48 +08:00
RTAkland f62055d2ac fix: fix repo language null throw npr 2024-10-08 16:58:05 +08:00
RTAkland 9e198c0084 chore: suppress warning 2024-10-08 16:54:03 +08:00
RTAkland 7dda9ad4bc feat: add more language color 2024-10-08 16:49:45 +08:00
RTAkland 80557e9ccc feat: add the history of today command 2024-10-08 15:15:51 +08:00
RTAkland e4d6e683d3 feat: add the cat command 2024-10-08 15:08:27 +08:00
RTAkland 97e62d5422 chore: format code 2024-10-08 14:44:52 +08:00
RTAkland a850ab9152 chore: remove unused language color 2024-10-08 11:55:08 +08:00
RTAkland 33f4014bae feat: add more language color 2024-10-08 11:54:09 +08:00
RTAkland 327426988b chore: chore: chore 2024-10-07 20:44:26 +08:00
RTAkland 938ef0792a feat: add pay for manzi sub command 2024-10-07 20:43:03 +08:00
RTAkland af9a117561 chore: add more command aliases 2024-10-07 20:18:50 +08:00
RTAkland 0ad3314ddb feat: add buy me a coffee command(打钱) 2024-10-07 20:12:04 +08:00
RTAkland b0ae938722 chore: introduce logger and save log into file 2024-10-07 19:13:51 +08:00
RTAkland 91906d9711 feat: add slime chunk helper command 2024-10-07 17:01:04 +08:00
RTAkland b7f82446f9 chore: format code 2024-10-07 16:11:06 +08:00
RTAkland 69743ca42b feat: add tts command 2024-10-07 16:07:07 +08:00
RTAkland 35fd368cb6 chore: format code 2024-10-07 15:36:03 +08:00
RTAkland 63c1de608d chore: format code 2024-10-07 12:47:10 +08:00
RTAkland c6f1bf9629 fix: fix bv parse 2024-10-07 12:44:31 +08:00
RTAkland ed85575e14 feat: support av code parse and use regex to match bv and av code instead of string split 2024-10-07 12:16:51 +08:00
RTAkland 2b725ab71e chore: format code 2024-10-06 21:21:05 +08:00
RTAkland 0a5c9b63c2 feat: add scan qrcode command 2024-10-06 21:18:23 +08:00
RTAkland 262cfa532c chore: nothing 2024-10-06 20:48:50 +08:00
RTAkland 956b29d076 chore: use local json file instead of api 2024-10-06 18:20:53 +08:00
RTAkland 8f7a5e5f0e chore: move root path 2024-10-06 18:07:32 +08:00
RTAkland 04b6823d7e chore: control 2024-10-06 11:28:56 +08:00
RTAkland 6bb6979467 feat: add scheduler to send like 2024-10-05 20:59:30 +08:00
RTAkland d42fd55228 feat: support translate image to zdjd 2024-10-05 20:05:13 +08:00
RTAkland 88a513864a feat: add zdjd command 2024-10-05 19:40:44 +08:00
RTAkland 98a6ebb3d4 chore: rename command name 2024-10-05 19:14:45 +08:00
RTAkland 4078af7c29 chore: add another method for base64 util 2024-10-05 19:08:45 +08:00
RTAkland 05efdaebff chore: suppress deprecated class warning 2024-10-05 16:36:51 +08:00
RTAkland 9abbf2830a chore: nothing 2024-10-04 20:52:54 +08:00
RTAkland c9c0f8e360 chore: fix jupyter language color 2024-10-04 15:21:14 +08:00
RTAkland b82a895d99 feat: add more language color 2024-10-04 12:36:56 +08:00
RTAkland 5f0921410d bump rob to 1.12.7, simple-java-mail to 8.12.1 2024-10-04 12:15:24 +08:00
RTAkland 45fafd4a4d feat: add random song command 2024-10-03 17:15:12 +08:00
RTAkland d7d7e237e6 feat: add latest commmit info and add language name on github repo card 2024-10-03 16:59:07 +08:00
RTAkland a349687e4f chore: update github repo parser 2024-10-03 16:35:38 +08:00
RTAkland 385dcb4328 chore: update comment 2024-10-03 11:12:26 +08:00
RTAkland 02ab1e816f chore: add command description 2024-10-02 21:43:09 +08:00
RTAkland 93338a9d64 feat: add query mc skin command 2024-10-02 21:41:28 +08:00
RTAkland 3a033d2694 chore: use application plugin instead of MANIFEST attribute 2024-10-02 21:10:17 +08:00
RTAkland 57885473bd feat: add command execute record in db 2024-10-02 21:02:05 +08:00
RTAkland 767bd7c0f3 docs: update docs 2024-10-02 20:05:52 +08:00
RTAkland 4ac5d6603b chore: update compiler command 2024-10-02 18:32:47 +08:00
RTAkland 9ba1c135d8 chore: bump rob to 1.12.2 2024-10-02 15:37:18 +08:00
RTAkland a07560fa37 chore: reduce reaction count 2024-10-01 20:20:46 +08:00
RTAkland 2eb8a3e387 feat: add reaction 2024-10-01 20:18:19 +08:00
RTAkland 805c6d56c2 feat: add reaction command 2024-10-01 18:54:25 +08:00
261 changed files with 4456 additions and 683 deletions

No files matched your search

+19 -5
View File
@@ -15,13 +15,14 @@
# 概述
> 这是一个使用了 `ROneBot` 框架的QQ机器人, 包含了20多种命令, 例如: 点歌, 今日人品, 签到...
> 这是一个使用了 `ROneBot` 框架的QQ机器人, 包含了超多命令, 例如: 点歌, 今日人品, 签到...
> 你可以在[这里](src/main/kotlin/cn/rtast/fancybot/commands/)来查看所有的命令
> ROneBot项目地址: [Gitlab](https://repo.rtast.cn/RTAkland/ronebot) [Github](https://github.com/RTAkland/ROneBot)
# 构建
由于项目使用的依赖过多项目构建出的产物在28Mb左右并且项目开启了
由于项目使用的依赖过多项目构建出的产物在40+Mb左右并且项目开启了
Github Action来自动化部署机器人, github Action的作用仅用作判断
构建是否成功以及自动部署机器人, 并不带有上传构建产物的功能(占用的空间太多)
所以你需要自行构建出机器人的jar包来运行
@@ -33,10 +34,23 @@ $ ./gradlew build clean
> 产物在`build/libs/`目录下你可以找到`-all`结尾的jar包
# 一些想说的话
# 部署
OneBot协议本身并不是一个很好的协议, 在Kritor、Laana等新型协议完善后机器人会全面迁移到上述协议中
并且将OneBot版本的分支放置到另外一个分支中不再更新, 后续将主要更新Kritor/Laana的分支
> 部署并不是一件很难的事, 只是需要准备的前期工作有点多
1. 申请一个Github Classic Token (需要`repo`的所有权限)(如果你想使用github作为图床)
2. 申请一个Cloudflare Token (需要`R2`的编辑权限)(如果你想使用cf作为图床)
3. 获取Cloudflare的Account Id以及R2的ID
4. 一个天聚数行API(TianAPI),并开通历史上的今天API权限
5. 一个openai平台的密钥和模型以及镜像地址
6. 和风天气的API key(免费订阅)
7. smtp服务器和端口
8. 网易云音乐API
9. QQ音乐API
10. 一个Azure APP (用于获取MC AccessToken)
11. 一个RTAST api token (用于生短链接, 你可以自行搭建但是源代码不公开)
12. 一个APISpace key
13. llama的api地址
# 开源
+25 -14
View File
@@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
alias(libs.plugins.shadow)
id("application")
}
val fancyBotVersion: String by project
@@ -14,8 +15,12 @@ version = fancyBotVersion
repositories {
mavenCentral()
mavenLocal()
maven("https://jitpack.io")
maven("https://repo.opencollab.dev/main/")
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven("https://repo.rtast.cn/api/v4/projects/33/packages/maven")
maven("https://repo.rtast.cn/api/v4/projects/19/packages/maven")
maven("https://repo.rtast.cn/api/v4/projects/3/packages/maven")
}
tasks.compileKotlin {
@@ -29,16 +34,26 @@ tasks.compileJava {
dependencies {
implementation(libs.rOneBot)
implementation(libs.ronebot)
implementation(libs.okhttp)
implementation(libs.exposedCore)
implementation(libs.exposedJDBC)
implementation(libs.sqliteJDBC)
implementation(libs.motdPinger)
implementation(libs.exposed.core)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.dao)
implementation(libs.sqlite.jdbc)
implementation(libs.mysql.jdbc)
implementation(libs.postgressql.jdbc)
implementation(libs.motd.pinger)
implementation(libs.animated.gif.lib)
implementation(libs.simple.java.mail)
implementation(libs.jsoup)
implementation(libs.nashorn.core)
implementation(libs.zxing.core)
implementation(libs.zxing.javase)
implementation(libs.logback.classic)
implementation(libs.slf4j.api)
implementation(libs.aws.s3.sdk)
implementation(libs.rconlib)
implementation(libs.mcprotocollib)
}
tasks.build {
@@ -49,15 +64,6 @@ tasks.jar {
enabled = false
}
tasks.shadowJar {
manifest {
attributes(
"Main-Class" to "cn.rtast.fancybot.FancyBotKt",
"Manifest-Version" to "1.0"
)
}
}
tasks.register("deployBot") {
val apiKey = System.getenv("MCSM_API_KEY")
val apiUrl = System.getenv("MCSM_API_URL")
@@ -65,3 +71,8 @@ tasks.register("deployBot") {
val instanceId = System.getenv("MCSM_INSTANCE_ID")
MCSMDeploy(apiUrl, apiKey, daemonId, instanceId).deploy(file("build/libs/FancyBot-1.0.0-all.jar"))
}
application {
mainClass = "cn.rtast.fancybot.FancyBotKt"
applicationName = "FancyBot"
}
+27 -9
View File
@@ -1,27 +1,45 @@
[versions]
kotlinVersion = "2.0.10"
ronebotVersion = "1.11.8"
kotlinVersion = "2.0.20"
ronebotVersion = "1.14.1"
shadowVersion = "8.3.0"
okhttpVersion = "5.0.0-alpha.14"
exposedVersion = "0.53.0"
sqliteJDBCVersion = "3.46.1.0"
motdPingerVersion = "1.1.2"
motdPingerVersion = "1.1.4"
animatedGifLibVersion = "1.4"
simpleJavaMailVersion = "8.11.3"
simpleJavaMailVersion = "8.12.2"
jsoupVersion = "1.18.1"
nashornVersion = "15.4"
zxingVersion = "3.5.3"
logbackClassicVersion = "1.5.8"
slf4jVersion = "2.0.16"
awsS3SDKVersion = "2.28.18"
rconLibVersion = "1.1.0"
mysqlConnectorVersion = "9.1.0"
postgresqlVersion = "42.7.4"
mcprotolobVersion = "1.21-SNAPSHOT"
[libraries]
rOneBot = { group = "cn.rtast", name = "ROneBot", version.ref = "ronebotVersion" }
motdPinger = { group = "cn.rtast", name = "MotdPinger", version.ref = "motdPingerVersion" }
ronebot = { group = "cn.rtast", name = "ROneBot", version.ref = "ronebotVersion" }
motd-pinger = { group = "cn.rtast", name = "MotdPinger", version.ref = "motdPingerVersion" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttpVersion" }
exposedCore = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposedVersion" }
exposedJDBC = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposedVersion" }
sqliteJDBC = { group = "org.xerial", name = "sqlite-jdbc", version.ref = "sqliteJDBCVersion" }
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposedVersion" }
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposedVersion" }
exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposedVersion" }
sqlite-jdbc = { group = "org.xerial", name = "sqlite-jdbc", version.ref = "sqliteJDBCVersion" }
mysql-jdbc = { group = "com.mysql", name = "mysql-connector-j", version.ref = "mysqlConnectorVersion" }
postgressql-jdbc = { group = "org.postgresql", name = "postgresql", version.ref = "postgresqlVersion" }
animated-gif-lib = { group = "com.madgag", name = "animated-gif-lib", version.ref = "animatedGifLibVersion" }
simple-java-mail = { group = "org.simplejavamail", name = "simple-java-mail", version.ref = "simpleJavaMailVersion" }
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoupVersion" }
nashorn-core = { group = "org.openjdk.nashorn", name = "nashorn-core", version.ref = "nashornVersion" }
zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxingVersion" }
zxing-javase = { group = "com.google.zxing", name = "javase", version.ref = "zxingVersion" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassicVersion" }
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4jVersion" }
aws-s3-sdk = { group = "software.amazon.awssdk", name = "s3", version.ref = "awsS3SDKVersion" }
rconlib = { group = "cn.rtast", name = "RCONLib", version.ref = "rconLibVersion" }
mcprotocollib = { group = "org.geysermc.mcprotocollib", name = "protocol", version.ref = "mcprotolobVersion" }
[bundles]
+70 -34
View File
@@ -4,29 +4,37 @@
* Date: 2024/8/27
*/
@file:Suppress("DEPRECATION")
package cn.rtast.fancybot
import cn.rtast.fancybot.commands.AboutCommand
import cn.rtast.fancybot.commands.EchoCommand
import cn.rtast.fancybot.commands.HelpCommand
import cn.rtast.fancybot.commands.ShuffleEchoCommand
import cn.rtast.fancybot.commands.StatusCommand
import cn.rtast.fancybot.commands.SupportMeCommand
import cn.rtast.fancybot.commands.lookup.*
import cn.rtast.fancybot.commands.misc.*
import cn.rtast.fancybot.commands.misc.JrrpCommand
import cn.rtast.fancybot.commands.misc.ShortLinkCommand
import cn.rtast.fancybot.commands.niuzi.*
import cn.rtast.fancybot.commands.niuzi.NiuziRedeemCommand
import cn.rtast.fancybot.commands.parse.AsciiArtCommand
import cn.rtast.fancybot.commands.parse.ReverseGIFCommand
import cn.rtast.fancybot.commands.misc.JrrpCommand
import cn.rtast.fancybot.commands.niuzi.NiuziRedeemCommand
import cn.rtast.fancybot.commands.misc.ShortLinkCommand
import cn.rtast.fancybot.entity.Config
import cn.rtast.fancybot.entity.enums.ImageType
import cn.rtast.fancybot.entity.enums.WSType
import cn.rtast.fancybot.enums.DatabaseType
import cn.rtast.fancybot.enums.ImageBedType
import cn.rtast.fancybot.enums.ImageType
import cn.rtast.fancybot.enums.WSType
import cn.rtast.fancybot.items.*
import cn.rtast.fancybot.util.file.BlackListManager
import cn.rtast.fancybot.util.file.ConfigManager
import cn.rtast.fancybot.util.file.NiuziBankManager
import cn.rtast.fancybot.util.file.NiuziManager
import cn.rtast.fancybot.util.file.RCONManager
import cn.rtast.fancybot.util.item.ItemManager
import cn.rtast.rob.ROneBotFactory
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.time.Instant
@@ -37,8 +45,19 @@ val gson: Gson = GsonBuilder()
.create()
const val ROOT_PATH = "./data"
const val ASSETS_BASE_URL = "https://static.rtast.cn"
const val API_RTAST_URL = "https://api.rtast.cn"
const val PBI_API_URL = "https://pbi.us.kg"
val DEFAULT_CONFIG = Config(
database = Config.Database(
DatabaseType.SQLite,
"127.0.0.1",
3306,
"root",
"114514",
"fancybot"
),
ncmAPI = "https://ncm.rtast.cn",
wsAddress = "ws://127.0.0.1",
wsType = WSType.Client,
@@ -61,14 +80,29 @@ val DEFAULT_CONFIG = Config(
llamaUrl = "http://127.0.0.1",
llamaModel = "llama3.1",
qqMusicApiUrl = "http://127.0.0.1:3200",
startUpNoticeUser = 114514L,
selfId = 114514L
noticeUser = 114514L,
selfId = 114514L,
tianXingApiKey = "1145141919810",
githubUser = "RTAkland",
githubImageRepo = "Static-Images",
imageBedType = ImageBedType.Github,
cloudflareAccountId = "111",
cloudflareR2AccessKeyId = "11111",
cloudflareR2SecretKey = "222",
cloudflareR2BucketName = "fancybot",
cloudflareR2PublicUrl = "https://114514.com",
apiSpaceKey = "114514",
apiRtastKey = "114514",
azureAppClientId = "114514",
youtubeDataApiKey = "114514"
)
val configManager = ConfigManager()
val itemManager = ItemManager()
val niuziManager = NiuziManager()
val niuziBankManager = NiuziBankManager()
val blackListManager = BlackListManager()
val rconManager = RCONManager()
val START_UP_TIME = Instant.now().epochSecond
@@ -78,31 +112,33 @@ val items = listOf(
TenSetuItem(), TenSetuR18Item()
)
val commands = listOf(
EchoCommand(), JrrpCommand(),
NiuziRedeemCommand(), HitokotoCommand(),
KFCCommand(), QRCodeCommand(),
AntiRevokeCommand(), MCPingCommand(),
WeatherCommand(), CigaretteCommand(),
RemakeCommand(), PixivCommand(),
RUACommand(), NslookupCommand(),
CompilerCommand(), AICommand(),
GithubUserCommand(), AboutCommand(),
MusicPlayUrlCommand(), DomainPriceCommand(),
SendMailCommand(), ZiBiCommand(),
UnsetZiBiCommand(), WikipediaCommand(),
AsciiArtCommand(), StatusCommand(),
JueCommand(), ShortLinkCommand(),
ShotOtherCommand(), ReverseGIFCommand(),
HelpCommand(), MusicCommand(),
LikeMeCommand(), NiuziTransferCommand(),
NiuziSignCommand(), NiuziJiJianCommand(),
NiuziQueryCommand(), MyNiuziCommand(),
NiuziBankCommand(), WithdrawCommand(),
BankTransferCommand(), CreateBankAccountCommand(),
BankBalanceCommand(), DepositCommand(),
DMSearchCommand(), LlamaCommand(),
QQMusicCommand(), BullShitGenerateCommand(),
NiuziRankCommand(), NiuziBankRankCommand(),
ShotSelfCommand(), TenSetuCommand()
val tasks = mapOf(
10000000L to suspend {
configManager.admins.forEach {
ROneBotFactory.action.sendLike(it, 1)
}
}
)
val commands = listOf(
EchoCommand(), JrrpCommand(), NiuziRedeemCommand(), HitokotoCommand(),
KFCCommand(), GenerateQRCodeCommand(), AntiRevokeCommand(), MCPingCommand(),
WeatherCommand(), CigaretteCommand(), RemakeCommand(), PixivCommand(),
RUACommand(), NslookupCommand(), CompilerCommand(), AICommand(),
GithubUserCommand(), AboutCommand(), MusicPlayUrlCommand(), DomainPriceCommand(),
SendMailCommand(), ZiBiCommand(), UnsetZiBiCommand(), WikipediaCommand(),
AsciiArtCommand(), StatusCommand(), JueCommand(), ShortLinkCommand(),
ShotOtherCommand(), ReverseGIFCommand(), HelpCommand(), MusicCommand(),
SendLikeCommand(), NiuziTransferCommand(), NiuziSignCommand(), NiuziJiJianCommand(),
NiuziQueryCommand(), MyNiuziCommand(), NiuziBankCommand(), WithdrawCommand(),
BankTransferCommand(), CreateBankAccountCommand(), BankBalanceCommand(), DepositCommand(),
AnimeSearchCommand(), LlamaCommand(), QQMusicCommand(), BullShitGenerateCommand(),
NiuziRankCommand(), NiuziBankRankCommand(), ShotSelfCommand(), TenSetuCommand(),
ReactionCommand(), MCSkinCommand(), GithubLatestCommitCommand(), RandomMusicCommand(),
ZDJDCommand(), ScanQRCodeCommand(), TTSCommand(), SlimeChunkHelperCommand(),
SupportMeCommand(), TheCatCommand(), TheHistoryOfTodayCommand(), MCVersionCommand(),
MinecraftWikiCommand(), GaoKaoDaysRemainCommand(), IdiomExplainCommand(), NiuziManagerCommand(),
TodayEatCommand(), TodayDrinkCommand(), MCLoginCommand(), RCONCommand(),
GravatarCommand(), PastebinCommand(), ShuffleEchoCommand(), TodayDontEatCommand(),
TodayDontDrinkCommand(), MCBotCommand(), HTTPCatCommand(), HTTPDogCommand()
)
+73 -28
View File
@@ -7,14 +7,26 @@
package cn.rtast.fancybot
import cn.rtast.fancybot.commands.misc.PastebinCommand
import cn.rtast.fancybot.commands.misc.ReactionCommand
import cn.rtast.fancybot.commands.misc.ScanQRCodeCommand
import cn.rtast.fancybot.commands.misc.ShortLinkCommand.Companion.makeShortLink
import cn.rtast.fancybot.commands.parse.*
import cn.rtast.fancybot.entity.enums.WSType
import cn.rtast.fancybot.util.initDatabase
import cn.rtast.fancybot.commands.reply.ImageBedCommand
import cn.rtast.fancybot.enums.WSType
import cn.rtast.fancybot.util.*
import cn.rtast.fancybot.util.misc.convertToDate
import cn.rtast.fancybot.util.misc.initBackgroundTasks
import cn.rtast.fancybot.util.misc.initCommand
import cn.rtast.fancybot.util.misc.initFilesDir
import cn.rtast.fancybot.util.misc.initItems
import cn.rtast.fancybot.util.misc.initSetuIndex
import cn.rtast.rob.ROneBotFactory
import cn.rtast.rob.entity.*
import cn.rtast.rob.entity.lagrange.FileEvent
import cn.rtast.rob.entity.lagrange.PokeEvent
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.enums.QQFace
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import kotlinx.coroutines.CoroutineScope
@@ -25,10 +37,11 @@ import java.net.URI
class FancyBot : OneBotListener {
private val logger = Logger.getLogger<FancyBot>()
private val coroutineScope = CoroutineScope(Dispatchers.IO)
override suspend fun onWebsocketOpenEvent() {
this.sendPrivateMessage(configManager.startUpNoticeUser, "FancyBot启动完成~")
this.sendPrivateMessage(configManager.noticeUser, "FancyBot启动完成~")
}
override suspend fun onGroupMessage(message: GroupMessage, json: String) {
@@ -37,43 +50,72 @@ class FancyBot : OneBotListener {
val msg = message.rawMessage
val groupId = message.groupId
val messageId = message.messageId
println("$sender($senderId: $groupId >>> $messageId): $msg")
logger.info("$sender($senderId: $groupId >>> $messageId): $msg")
logger.trace("$sender($senderId: $groupId >>> $messageId: $json")
if (message.message.any { it.type == ArrayMessageType.face && it.data.id.toString() == "419" }) {
message.reply("你发牛魔的火车呢, 我直接就是打断")
}
if (message.rawMessage.contains("原神")) {
(0..10).forEach { _ ->
message.reaction(QQFace.entries.random())
}
message.reply("你原神牛魔呢")
}
GitHubParseCommand.parse(message)
ReverseGIFCommand.callback(message)
AsciiArtCommand.callback(message)
ScanQRCodeCommand.callback(message)
BiliVideoParseCommand.parse(this, message)
BiliUserParseCommand.parse(this, message)
DouyinVideoParseCommand.parse(message)
YoutubeVideoParseCommand.parse(message)
if (message.rawMessage.toList().any { it in arrayListOf('*', '-', '/', '+', '=') }) {
val calculateResult = CalculateCommand.parse(message.rawMessage)
calculateResult?.let { message.reply(calculateResult) }
}
if (message.message.any { it.type == ArrayMessageType.reply }) { // Image url parse
if (message.message.any { it.type == ArrayMessageType.reply }) {
val command = message.message.reversed().find { it.type == ArrayMessageType.text }!!.data.text!!
val replyId = message.message.find { it.type == ArrayMessageType.reply }!!.data.id!!
if (command.contains("图来") || command.contains("图链")) {
val getMsg = this.getMessage(replyId.toString().toLong())
val plainTextContent = getMsg.message
.filter { it.type == ArrayMessageType.text }
.joinToString { it.data.text!! }
if (command.contains("倒放") || command.contains("/df")) {
// 使用回复消息的方式对一个gif倒放
ReverseGIFCommand.reverse(message, getMsg)
}
if (command.contains("/sl") || command.contains("/short")) {
// 使用回复消息的方式快速对一个url消息创建一个短链接
message.reply(plainTextContent.trim().makeShortLink())
}
if (command.contains("/pb") || command.contains("/pastebin")) {
// 使用回复消息的方式快速将创建一个pastebin
val pastebinUrl = PastebinCommand.createPastebin(plainTextContent)
message.reply(pastebinUrl)
}
if (command.contains("/ascii") || command.contains("/asc")) {
// 使用回复消息的方式直接对一个图片进行生成Ascii art的操作
val url = AsciiArtCommand.getImageUrl(getMsg)
val image = AsciiArtCommand.generateAsciiArt(url)
message.reply(image)
}
if (command.contains("图来") || command.contains("图链")) {
// 获取一个或多个图片的链接, 并且生成一个或多个短链接
ImageURLCommand.callback(message, getMsg)
}
if (command.contains("reaction")) {
// 使用reaction刷屏回应一条消息
ReactionCommand.reaction(this, message, getMsg.messageId)
}
if (message.rawMessage.startsWith("https://github.com/") || message.rawMessage.startsWith("git@github.com:")) {
GitHubParseCommand.parse(this, message)
if (command.contains("图床")) {
// 将一个图片上传到图床
ImageBedCommand.execute(getMsg, message)
}
if (message.rawMessage.startsWith("BV") ||
message.rawMessage.startsWith("https://www.bilibili.com") ||
message.rawMessage.contains("https://b23.tv/") ||
message.message.find { it.type == ArrayMessageType.json } != null
) {
BVParseCommand.parse(this, message)
}
coroutineScope.launch {
@@ -81,7 +123,7 @@ class FancyBot : OneBotListener {
if (it.type == ArrayMessageType.image) {
val filename = it.data.file!!.split("=").last() + ".png"
URI(it.data.file!!).toURL().openConnection().inputStream.use { input ->
File("./files/images/$filename").outputStream().use { output ->
File("$ROOT_PATH/caches/images/$filename").outputStream().use { output ->
output.write(input.readBytes())
}
}
@@ -109,7 +151,7 @@ class FancyBot : OneBotListener {
}
override suspend fun onGroupFileUpload(event: FileEvent) {
event.saveTo("./files")
event.saveTo("$ROOT_PATH/caches/")
}
override suspend fun onLeaveEvent(groupId: Long, userId: Long, operator: Long, time: Long) {
@@ -123,21 +165,23 @@ class FancyBot : OneBotListener {
this.sendGroupMessage(groupId, msg)
}
override suspend fun onPoke(event: PokeEvent) {
event.groupId?.let {
override suspend fun onGroupPoke(event: PokeEvent) {
if (event.targetId == configManager.selfId) {
val msg = MessageChain.Builder()
.addAt(event.userId)
.addText("${event.action.first()}牛魔呢")
.build()
this.sendGroupMessage(it, msg)
}
}
this.sendGroupMessage(event.groupId!!, msg)
}
}
fun initFilesDir() {
File("./files/images").also { it.mkdirs() }
override suspend fun onBeKicked(groupId: Long, operator: Long, time: Long) {
blackListManager.insertGroup(groupId, operator, time)
this.sendPrivateMessage(
configManager.noticeUser,
"被: ${groupId}踢出! 操作人: $operator 时间: ${time.convertToDate()} 已将其拉入黑名单!"
)
}
}
suspend fun main() {
@@ -155,7 +199,8 @@ suspend fun main() {
}
initDatabase()
initFilesDir()
val commandManager = rob.commandManager
commands.forEach { commandManager.register(it) }
items.forEach { itemManager.register(it) }
initCommand(rob)
initSetuIndex()
initItems()
initBackgroundTasks(rob)
}
@@ -8,6 +8,9 @@
package cn.rtast.fancybot.commands
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
@@ -17,6 +20,25 @@ class EchoCommand : BaseCommand() {
override val commandNames = listOf("/echo")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (message.sender.isAdmin || message.sender.isOwner) {
listener.sendGroupMessage(message.groupId, args.joinToString(" "))
insertActionRecord(CommandAction.Echo, message.sender.userId)
} else {
message.reply("你不许用echo")
}
}
}
@CommandDescription("精神错乱(×), 胡言乱语(√)")
class ShuffleEchoCommand : BaseCommand() {
override val commandNames = listOf("/secho", "/se")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (message.sender.isAdmin || message.sender.isOwner) {
message.reply(message.rawMessage.toList().shuffled().joinToString(""))
insertActionRecord(CommandAction.Echo, message.sender.userId)
} else{
message.reply("你不许用echo")
}
}
}
@@ -26,13 +26,12 @@ class HelpCommand : BaseCommand() {
if (args.isEmpty()) {
val node = NodeMessageChain.Builder()
val msg = MessageChain.Builder()
commands.sortedBy { it::class.simpleName }.forEach {
commands.sortedBy { it::class.simpleName }.toMutableList().forEach {
val description = if (!it::class.hasAnnotation<CommandDescription>()) "暂无描述"
else it::class.findAnnotation<CommandDescription>()?.description!!
val commandName = it::class.simpleName?.replace("Command", "")!!
val commandNames = it.commandNames.joinToString(",")
msg.addText("[$commandName] [$description] 命令: $commandNames")
.addNewLine()
msg.addText("[$commandName] [$description] 命令: $commandNames").addNewLine()
}
msg.addText("共计${commands.size}条命令")
node.addMessageChain(msg.build(), configManager.selfId)
@@ -9,6 +9,8 @@ package cn.rtast.fancybot.commands
import cn.rtast.fancybot.START_UP_TIME
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -44,5 +46,6 @@ class StatusCommand : BaseCommand() {
.addText("Java版本: $javaVersion / Kotlin版本: $kotlinVersion")
.build()
message.reply(msg)
insertActionRecord(CommandAction.Status, message.sender.userId)
}
}
@@ -0,0 +1,40 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/7
*/
package cn.rtast.fancybot.commands
import cn.rtast.fancybot.ASSETS_BASE_URL
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import java.net.URI
@CommandDescription("给我打钱!")
class SupportMeCommand : BaseCommand() {
override val commandNames = listOf("打钱", "给我打钱", "给满子打钱")
override suspend fun executeGroup(
listener: OneBotListener,
message: GroupMessage,
args: List<String>,
matchedCommand: String
) {
if (matchedCommand == "给满子打钱") {
message.reply("你打牛魔呢?")
return
}
val image = URI("$ASSETS_BASE_URL/images/048cc8af57f19850ca176f29e50b6215.png")
.toURL().readBytes().encodeToBase64()
val msg = MessageChain.Builder()
.addImage(image, true)
.build()
message.reply(msg)
}
}
@@ -13,7 +13,9 @@ import cn.rtast.fancybot.entity.gpt.ChatCompletionsPayload
import cn.rtast.fancybot.entity.gpt.ChatCompletionsResponse
import cn.rtast.fancybot.entity.gpt.LlamaResponse
import cn.rtast.fancybot.entity.gpt.ModelList
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
@@ -68,6 +70,7 @@ class AICommand : BaseCommand() {
.build()
nodeMsg.addMessageChain(msg, configManager.selfId)
listener.sendGroupForwardMsg(message.groupId, nodeMsg.build())
insertActionRecord(CommandAction.AI, message.sender.userId, "$content-$model-GPT")
}
}
@@ -94,5 +97,6 @@ class LlamaCommand : BaseCommand() {
.build()
nodeMsg.addMessageChain(msg, configManager.selfId)
listener.sendGroupForwardMsg(message.groupId, nodeMsg.build())
insertActionRecord(CommandAction.AI, message.sender.userId, "$prompt-LLAMA")
}
}
@@ -20,8 +20,8 @@ import cn.rtast.rob.util.ob.OneBotListener
import okhttp3.OkHttpClient
import okhttp3.Request
@CommandDescription("搜索动漫")
class DMSearchCommand : BaseCommand() {
@CommandDescription("搜索动漫(Bangumi.tv)")
class AnimeSearchCommand : BaseCommand() {
override val commandNames = listOf("/dm", "动漫")
private val client = OkHttpClient()
@@ -61,6 +61,6 @@ class CigaretteCommand : BaseCommand() {
.build()
nodeMsg.addMessageChain(footerMsg, configManager.selfId)
message.reply(nodeMsg.build())
message.sender.groupPoke()
message.sender.poke()
}
}
@@ -11,9 +11,9 @@ import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.entity.domain.PricePayload
import cn.rtast.fancybot.entity.domain.PriceResponse
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.fancybot.util.toByteArray
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -0,0 +1,58 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/3
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.commands.parse.GitHubParseCommand
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.github.LatestCommit
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.str.fromArrayJson
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("查询Github上的仓库的最新一次的提交信息")
class GithubLatestCommitCommand : BaseCommand() {
override val commandNames = listOf(".commit")
private val userInfoUrl = "https://api.github.com/repos"
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("发送`.commit <用户名/仓库名>`即可查询仓库最新一次的提交信息")
}
try {
val (user, repo) = if (args.size == 1) {
args.first().split("/").first() to args.first().split("/").last()
} else {
args.first() to args.last()
}
val response = Http.get(
"$userInfoUrl/$user/$repo/commits",
headers = mapOf("Authorization" to configManager.githubKey)
).fromArrayJson<List<LatestCommit>>().first()
val image = GitHubParseCommand.creatRepoImage(user, repo)
val msg = MessageChain.Builder()
.addImage(image, true)
.addText("最新提交ID: ${response.sha.substring(0, 7)}")
.addNewLine()
.addText("提交者: ${response.commit.committer.name}(${response.commit.committer.email})")
.addNewLine()
.addText("提交日期: ${response.commit.committer.date}")
.addNewLine()
.addText("https://github.com/$user/$repo")
.build()
message.reply(msg)
} catch (e: Exception) {
e.printStackTrace()
message.reply("查询失败~")
}
}
}
@@ -11,11 +11,11 @@ import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.github.UserInfo
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.drawCustomImage
import cn.rtast.fancybot.util.drawString
import cn.rtast.fancybot.util.misc.drawCustomImage
import cn.rtast.fancybot.util.misc.drawString
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.toBufferedImage
import cn.rtast.fancybot.util.toByteArray
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -0,0 +1,34 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/14
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.getMD5
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("查询用户的Gravatar头像")
class GravatarCommand : BaseCommand() {
override val commandNames = listOf("/g", "/gravatar")
companion object {
private const val GRAVATAR_URL = "https://gravatar.rtast.cn/avatar"
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val emailMD5 = if (args.first().trim().lowercase().contains("@")) args.first().trim().lowercase().getMD5()
else args.first().trim().lowercase()
val avatarImage = "$GRAVATAR_URL/$emailMD5?size=256".toURL().readBytes().encodeToBase64()
val msg = MessageChain.Builder().addImage(avatarImage, true).build()
message.reply(msg)
}
}
@@ -0,0 +1,51 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/10
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.apispace.IdiomExplainResponse
import cn.rtast.fancybot.util.Http
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("解释一个成语")
class IdiomExplainCommand : BaseCommand() {
override val commandNames = listOf("成语解释")
private val apiSpaceUrl = "https://eolink.o.apispace.com/cyudac/api/v1/idioms/SearchIdiomsByPinyin/search"
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("发送`成语解释 <成语>`就能解释这个成语的意思了")
return
}
try {
val idiom = args.joinToString(" ").trim()
val response = Http.get<IdiomExplainResponse>(
apiSpaceUrl,
mapOf("name" to idiom, "search_type" to "name"),
mapOf("X-APISpace-Token" to configManager.apiSpaceKey)
).data.first()
val msg = MessageChain.Builder()
.addText("成语: ${response.name} 的解释如下")
.addNewLine()
.addText(response.explanation)
.addNewLine(2)
.addText(response.provenance)
.addNewLine(2)
.addText(response.sound)
.build()
message.reply(msg)
} catch (_: Exception) {
message.reply("这个词似乎不是一个成语呢~~")
}
}
}
@@ -1,92 +0,0 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/9/3
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.motdpinger.BedrockPing
import cn.rtast.motdpinger.JavaPing
import cn.rtast.motdpinger.removeColorCodes
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import java.net.SocketException
@CommandDescription("PingMC服务器")
class MCPingCommand : BaseCommand() {
override val commandNames = listOf("/mcping")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
val msg = MessageChain.Builder()
.addText("使用方法: /mcping <host>:[port] [java|be]")
.addNewLine()
.addText("不添加平台默认Java版~")
.build()
message.reply(msg)
return
}
val platform = if (args.size == 2) args.last() else "java"
when (platform) {
"java", "je", "Java", "JAVA" -> {
val parts = args.first().split(":")
val port = if (parts.size == 1) {
25565
} else {
parts.last().toInt()
}
val host = parts.first()
val response = JavaPing().ping(host, port, 10000)
if (response == null) {
message.reply("无法获取服务器的消息请检查输入是否正确 >>> $host:$port")
return
}
val msg = MessageChain.Builder()
response.favicon?.let {
msg.addImage(response.favicon!!.replace("data:image/png;base64,", ""), true)
}
msg.addText("服务器地址: $host:$port | 服务器类型: Java")
.addNewLine()
.addText("服务器版本: ${response.version.name}/${response.version.protocol}")
.addNewLine()
.addText("在线玩家数: ${response.players.online}/${response.players.max}")
message.reply(msg.build())
}
"be", "bedrock", "BE", "Bedrock" -> {
val parts = args.first().split(":")
val port = if (parts.size == 1) {
19132
} else {
parts.last().toInt()
}
val host = parts.first()
try {
val response = BedrockPing().ping(host, port, 10000)
val msg = MessageChain.Builder()
.addText("服务器地址: $host:$port | 服务器类型: Bedrock")
.addNewLine()
.addText("服务器版本: ${response.version}/${response.protocolVersion}")
.addNewLine()
.addText("在线玩家: ${response.onlinePlayers}/${response.maxPlayers}")
.addNewLine()
.addText("MOTD: ${response.motd.removeColorCodes()}")
.addNewLine()
.addText(response.subTitle.removeColorCodes())
message.reply(msg.build())
} catch (_: SocketException) {
message.reply("无法从服务器接收Ping结果请检查服务器地址是否正确 >>> $host:$port")
}
}
else -> {
message.reply("输入错误请在 `java` 和 `be`中选择 >>> $platform")
}
}
}
}
@@ -8,27 +8,92 @@
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.IPGeo
import cn.rtast.fancybot.util.Http
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import cn.rtast.rob.util.ob.asNode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.InetAddress
import java.net.UnknownHostException
import java.util.Hashtable
import javax.naming.directory.InitialDirContext
@CommandDescription("解析域名获取IP地址")
class NslookupCommand : BaseCommand() {
override val commandNames = listOf("/ns")
companion object {
private const val MC_SRV_PREFIX = "_minecraft._tcp"
private const val IP_INFO_API = "https://ipinfo.io/###/json"
private val env = Hashtable<String, String>().also {
it["java.naming.factory.initial"] = "com.sun.jndi.dns.DnsContextFactory"
}
private val dirContext = InitialDirContext(env)
fun srv(host: String): MutableList<String> {
val srvDomain = "$MC_SRV_PREFIX.$host"
val attrs = dirContext.getAttributes(srvDomain, arrayOf("SRV"))
val srvRecords = mutableListOf<String>()
val srvAttr = attrs.get("SRV")
if (srvAttr != null) {
for (i in 0 until srvAttr.size()) {
val srvRecord = srvAttr.get(i) as String
srvRecords.add(srvRecord)
}
}
return srvRecords
}
}
private fun geo(ip: String) = Http.get<IPGeo>(IP_INFO_API.replace("###", ip))
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val host = args.joinToString(" ")
val host = args.first()
val nsType = if (args.size == 3) args[1] else "common"
val srvType = if (args.size == 3) args.last() else "mc"
try {
if (nsType == "srv") {
when (srvType) {
"mc" -> {
val srvRecords = srv(host)
val messages = mutableListOf<MessageChain>()
srvRecords.forEach {
val srv = it.split(" ")
val priority = srv.first().toInt()
val weight = srv[1].toInt()
val port = srv[2].toInt()
val host = srv.last()
val ip = InetAddress.getByName(host).hostAddress
val geo = this.geo(ip)
val msg = MessageChain.Builder()
.addText("域名: ${host}解析后的IP地址为: ")
.addText(withContext(Dispatchers.IO) { InetAddress.getByName(host) }.hostAddress)
.addText("解析后的地址: $host:$port")
.addNewLine()
.addText("权重: $weight | 优先级: $priority")
.addNewLine()
.addText("$host -> $ip")
.addNewLine()
.addText("国家: ${geo.country} 地区: ${geo.region} 城市: ${geo.city} | 所属组织: ${geo.org}")
.build()
messages.add(msg)
}
message.reply(messages.asNode(configManager.selfId))
}
}
} else {
val ip = withContext(Dispatchers.IO) { InetAddress.getByName(host) }.hostAddress
val geo = this.geo(ip)
val msg = MessageChain.Builder()
.addText("$host -> $ip")
.addNewLine()
.addText("国家: ${geo.country} 地区: ${geo.region} 城市: ${geo.city} | 所属组织: ${geo.org}")
.build()
message.reply(msg)
}
} catch (_: UnknownHostException) {
message.reply("未知的主机名: $host")
} catch (e: Exception) {
@@ -56,9 +56,11 @@ class PixivCommand : BaseCommand() {
} catch (_: Exception) {
}
}
node.addMessageChain(MessageChain.Builder()
node.addMessageChain(
MessageChain.Builder()
.addText("图片来源: Pixiv")
.build(), configManager.selfId)
.build(), configManager.selfId
)
message.reply(node.build())
return
}
@@ -1,27 +0,0 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/8/30
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("生成二维码")
class QRCodeCommand : BaseCommand() {
override val commandNames = listOf("/qr")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val content = args.joinToString(" ")
val msg = MessageChain.Builder()
.addImage("https://api.rtast.cn/api/generate_qr?data=$content")
.build()
message.reply(msg)
}
}
@@ -0,0 +1,35 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.entity.thecat.Cat
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.fromArrayJson
import cn.rtast.fancybot.util.str.proxy
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("随机猫猫图")
class TheCatCommand : BaseCommand() {
override val commandNames = listOf("随机猫咪", "随机猫猫")
private val theCatApiUrl = "https://api.thecatapi.com/v1/images/search"
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val imageBase64 = Http.get(theCatApiUrl)
.fromArrayJson<List<Cat>>().first().url
.proxy.toURL().readBytes().encodeToBase64()
val msg = MessageChain.Builder().addImage(imageBase64, true).build()
message.reply(msg)
}
}
@@ -0,0 +1,52 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.tianxing.HistoryOfToday
import cn.rtast.fancybot.util.Http
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import cn.rtast.rob.util.ob.asNode
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@CommandDescription("历史上的今天~~~")
class TheHistoryOfTodayCommand : BaseCommand() {
override val commandNames = listOf("历史上的今天")
private val tianXingApiUrl = "https://apis.tianapi.com/lishi/index"
private val tianXingApiKey = configManager.tianXingApiKey
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Shanghai"))
val formatter = DateTimeFormatter.ofPattern("MMdd")
val currentDate = currentDateTime.format(formatter)
val messages = mutableListOf<MessageChain>()
Http.get<HistoryOfToday>(
tianXingApiUrl,
mapOf(
"key" to tianXingApiKey,
"date" to currentDate,
)
).result.list.forEach {
messages.add(
MessageChain.Builder()
.addText("事件: ${it.title}")
.addNewLine()
.addText("发生日期: ${it.lsDate}")
.build()
)
}
message.reply(messages.asNode(configManager.selfId))
}
}
@@ -12,12 +12,12 @@ import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.weather.Geo
import cn.rtast.fancybot.entity.weather.Weather
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.Resources
import cn.rtast.fancybot.util.drawCenteredText
import cn.rtast.fancybot.util.drawCustomImage
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.misc.drawCenteredText
import cn.rtast.fancybot.util.misc.drawCustomImage
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.toBufferedImage
import cn.rtast.fancybot.util.toByteArray
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -10,7 +10,9 @@ package cn.rtast.fancybot.commands.lookup
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.entity.wiki.PageInfoResponse
import cn.rtast.fancybot.entity.wiki.WikipediaResponse
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -23,10 +25,12 @@ class WikipediaCommand : BaseCommand() {
private val wikipediaAPI = "https://proxy.rtast.cn/https/zh.wikipedia.org/w/api.php"
private fun String.extractPlainTextFromHtml(): String {
companion object {
fun String.extractPlainTextFromHtml(): String {
val doc = Jsoup.parse(this)
return doc.text()
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
@@ -69,6 +73,8 @@ class WikipediaCommand : BaseCommand() {
.addText("没有查询到指定的wiki页面呢 >>> $title")
.build()
message.reply(msg)
} finally {
insertActionRecord(CommandAction.Wiki, message.sender.userId, title)
}
}
}
@@ -8,7 +8,8 @@
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.rob.entity.ArrayMessage
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
@@ -20,13 +21,17 @@ class AntiRevokeCommand : BaseCommand() {
override val commandNames = listOf("/revoke", "/rv")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (message.sender.userId !in configManager.admins) message.reply("你不许用防撤回")
if (message.sender.isAdmin || message.sender.isOwner) {
val messageId = args.first().toLong()
val getMsg = listener.getMessage(messageId)
val msgList = mutableListOf<ArrayMessage>()
msgList.add(ArrayMessage(ArrayMessageType.at, ArrayMessage.Data(qq = getMsg.sender.userId.toString())))
msgList.add(ArrayMessage(ArrayMessageType.at, ArrayMessage.Data(qq = message.sender.userId.toString())))
msgList.add(ArrayMessage(ArrayMessageType.text, ArrayMessage.Data(text = "\n被撤回的消息如下: \n")))
msgList.addAll(getMsg.message)
listener.sendGroupMessage(message.groupId, msgList)
insertActionRecord(CommandAction.AntiRevoke, message.sender.userId, getMsg.messageId.toString())
} else {
message.reply("你不许用防撤回")
}
}
}
@@ -7,9 +7,10 @@
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.bullshit.BullShit
import cn.rtast.fancybot.util.Resources
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.str.fromJson
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
@@ -18,6 +19,7 @@ import cn.rtast.rob.util.ob.NodeMessageChain
import cn.rtast.rob.util.ob.OneBotListener
import kotlin.random.Random
@CommandDescription("狗屁不通文章生成器")
class BullShitGenerateCommand : BaseCommand() {
override val commandNames = listOf("/gpbt", "狗屁不通")
@@ -65,6 +67,6 @@ class BullShitGenerateCommand : BaseCommand() {
.addMessageChain(MessageChain.Builder().addText(finalText).build(), configManager.selfId)
.build()
message.reply(node)
message.sender.groupPoke()
message.sender.poke()
}
}
@@ -12,16 +12,18 @@ import cn.rtast.fancybot.entity.compiler.GLOTPayload
import cn.rtast.fancybot.entity.compiler.GLOTResponse
import cn.rtast.fancybot.entity.compiler.KCSPayload
import cn.rtast.fancybot.entity.compiler.KCSResponse
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("执行不同语言的代码片段")
@CommandDescription("执行不同语言的代码")
class CompilerCommand : BaseCommand() {
override val commandNames = listOf("/compiler", "/exec")
override val commandNames = listOf("/compiler", "/exec", "/e")
private val kotlinCompilerServer = "https://api.kotlinlang.org/api/2.0.20/compiler/run"
private val glotCompilerServer = "https://glot.io/run"
@@ -31,16 +33,22 @@ class CompilerCommand : BaseCommand() {
message.reply("使用`/exec` <语言> <代码> 即可执行~")
return
}
val language = args.first()
var language = args.first().lowercase()
val code = args.drop(1).joinToString(" ")
val response = when (language) {
"kotlin", "Kotlin", "kt", "Kt", "KT" -> {
"kotlin", "kt" -> {
Http.post<KCSResponse>(
kotlinCompilerServer, jsonBody = KCSPayload(listOf(KCSPayload.File(code))).toJson()
kotlinCompilerServer,
jsonBody = KCSPayload(listOf(KCSPayload.File(code))).toJson()
).text
}
else -> {
if (language == "js") language = "javascript"
if (language == "cs") language = "csharp"
if (language == "sh") language = "bash"
if (language == "asm") language = "assembly"
if (language == "py") language = "python"
Http.post<GLOTResponse>(
"$glotCompilerServer/$language",
params = mapOf("version" to "latest"),
@@ -48,6 +56,7 @@ class CompilerCommand : BaseCommand() {
).stdout
}
}
try {
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("执行结果如下:")
@@ -55,5 +64,10 @@ class CompilerCommand : BaseCommand() {
.addText(response.replace("<outStream>", "").replace("</outStream>", ""))
.build()
listener.sendGroupMessage(message.groupId, msg)
} catch (e: Exception) {
message.reply("执行错误: ${e.message}")
} finally {
insertActionRecord(CommandAction.Compiler, message.sender.userId)
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/9
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
import java.time.LocalDate
import java.time.temporal.ChronoUnit
@CommandDescription("高考倒计时")
class GaoKaoDaysRemainCommand : BaseCommand() {
override val commandNames = listOf("高考倒计时")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val gaoKaoDate = LocalDate.of(LocalDate.now().year, 6, 7)
val currentDate = LocalDate.now()
val adjustedGaoKaoDate = if (currentDate.isAfter(gaoKaoDate)) gaoKaoDate.plusYears(1) else gaoKaoDate
val daysBetween = ChronoUnit.DAYS.between(currentDate, adjustedGaoKaoDate)
message.reply("高考倒计时: ${daysBetween}")
}
}
@@ -0,0 +1,35 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/23
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.enums.HttpStatusCode
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("随机HTTP猫猫")
class HTTPCatCommand : BaseCommand() {
override val commandNames = listOf("http猫猫", "HTTP猫猫")
private val notFoundCatBase64 = Resources.loadFromResourcesAsBytes("httpcat/0.jpg")!!.encodeToBase64()
private fun loadHTTPCat(code: String): String {
return Resources.loadFromResourcesAsBytes("httpcat/${code}.jpg")?.encodeToBase64() ?: notFoundCatBase64
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val statusCode = if (args.isEmpty()) HttpStatusCode.entries.random().code.toString() else args.first()
val image = loadHTTPCat(statusCode)
val msg = MessageChain.Builder().addImage(image, true).build()
message.reply(msg)
}
}
@@ -0,0 +1,35 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/23
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.enums.HttpStatusCode
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("随机HTTP狗狗")
class HTTPDogCommand : BaseCommand() {
override val commandNames = listOf("http狗狗", "HTTP狗狗")
private val notFoundCatBase64 = Resources.loadFromResourcesAsBytes("httpdog/999.jpg")!!.encodeToBase64()
private fun loadHTTPDog(code: String): String {
return Resources.loadFromResourcesAsBytes("httpdog/${code}.jpg")?.encodeToBase64() ?: notFoundCatBase64
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val statusCode = if (args.isEmpty()) HttpStatusCode.entries.random().code.toString() else args.first()
val image = loadHTTPDog(statusCode)
val msg = MessageChain.Builder().addImage(image, true).build()
message.reply(msg)
}
}
@@ -8,11 +8,11 @@
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.util.Resources
import cn.rtast.fancybot.util.drawCircularImage
import cn.rtast.fancybot.util.makeGif
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.misc.drawCircularImage
import cn.rtast.fancybot.util.misc.makeGif
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.toBufferedImage
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
@@ -22,14 +22,15 @@ import java.time.ZoneId
class KFCCommand : BaseCommand() {
override val commandNames = listOf("/fkxqs", "/kfc")
private fun isThursday(): Boolean {
companion object {
fun isThursday(): Boolean {
val dateTime = LocalDateTime.ofInstant(
Instant.now(),
ZoneId.of("Asia/Shanghai")
)
return dateTime.dayOfWeek == DayOfWeek.THURSDAY
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (!isThursday()) {
@@ -0,0 +1,668 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/9
*/
@file:Suppress("KDocUnresolvedReference", "unused")
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.commands.lookup.NslookupCommand
import cn.rtast.fancybot.commands.lookup.WikipediaCommand.Companion.extractPlainTextFromHtml
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.db.deleteAccessTokenById
import cn.rtast.fancybot.db.getAccessTokenById
import cn.rtast.fancybot.db.insertNewAccessToken
import cn.rtast.fancybot.entity.mc.DecodedSkin
import cn.rtast.fancybot.entity.mc.MCVersion
import cn.rtast.fancybot.entity.mc.MCWikiPageResponse
import cn.rtast.fancybot.entity.mc.MCWikiSearchResponse
import cn.rtast.fancybot.entity.mc.Skin
import cn.rtast.fancybot.entity.mc.Username
import cn.rtast.fancybot.entity.mc.oauth.DeviceCodeResponse
import cn.rtast.fancybot.entity.mc.oauth.LoginXboxLivePayload
import cn.rtast.fancybot.entity.mc.oauth.LoginXboxLiveResponse
import cn.rtast.fancybot.entity.mc.oauth.ObtainXSTSPayload
import cn.rtast.fancybot.entity.mc.oauth.ObtainXSTSResponse
import cn.rtast.fancybot.entity.mc.oauth.RedeemCodeResponse
import cn.rtast.fancybot.enums.mc.MCVersionType
import cn.rtast.fancybot.rconManager
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.Logger
import cn.rtast.fancybot.util.mcbot.MCClient
import cn.rtast.fancybot.util.misc.isFullyTransparent
import cn.rtast.fancybot.util.misc.scaleImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.fancybot.util.str.convertToHTML
import cn.rtast.fancybot.util.str.decodeToString
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.fromJson
import cn.rtast.fancybot.util.str.generateRandomString
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.fancybot.util.str.uriEncode
import cn.rtast.motdpinger.BedrockPing
import cn.rtast.motdpinger.JavaPing
import cn.rtast.rcon.exceptions.AuthFailedException
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.entity.PrivateMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import cn.rtast.rob.util.ob.asMessageChain
import cn.rtast.rob.util.ob.asNode
import okhttp3.FormBody
import java.awt.AlphaComposite
import java.awt.Color
import java.awt.image.BufferedImage
import java.net.InetAddress
import java.net.SocketException
import java.net.URI
import java.util.concurrent.Executors
import javax.imageio.ImageIO
import kotlin.random.Random
import kotlin.text.split
@CommandDescription("获取MC最新的版本!")
class MCVersionCommand : BaseCommand() {
override val commandNames = listOf("mc版本")
private val javaEditionAPI = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val mcVersion = Http.get<MCVersion>(javaEditionAPI)
val msg = MessageChain.Builder()
.addText("最新正式版: ${mcVersion.latest.release}")
.addNewLine()
.addText("发布时间: ${mcVersion.versions.find { it.type == MCVersionType.release }?.releaseTime}")
.addNewLine()
.addText("最新测试版: ${mcVersion.latest.snapshot}")
.addNewLine()
.addText("测试版发布时间: ${mcVersion.versions.find { it.type == MCVersionType.snapshot }?.releaseTime}")
.build()
message.reply(msg)
}
}
@CommandDescription("查询来自minecraft.wiki中的wiki页面")
class MinecraftWikiCommand : BaseCommand() {
override val commandNames = listOf("/mcwiki")
private val baseApiUrl = "https://zh.minecraft.wiki/rest.php/v1"
private val logger = Logger.getLogger<MinecraftWikiCommand>()
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val action = args.first()
when (action) {
"s", "搜索" -> {
val searchKeyword = args.drop(1).joinToString(" ").trim()
val messages = mutableListOf<MessageChain>()
val headerMessage = listOf("结果如下:").asMessageChain()
messages.add(headerMessage)
Http.get<MCWikiSearchResponse>(
"$baseApiUrl/search/title",
mapOf("q" to searchKeyword, "limit" to 10)
).pages.forEach {
val msg = MessageChain.Builder()
val imageUrl = it.thumbnail
if (imageUrl != null) {
val imageBase64 = imageUrl.url.toURL().readBytes().encodeToBase64()
msg.addImage(imageBase64, true)
}
msg.addText("标题: ${it.title}")
.addNewLine()
.addText("ID: ${it.id}")
.addNewLine()
.addText("https://zh.minecraft.wiki/w/${it.title.uriEncode}")
messages.add(msg.build())
}
message.reply(messages.asNode(configManager.selfId))
}
else -> {
val title = args.joinToString(" ").trim()
var response = Http.get<MCWikiPageResponse>("$baseApiUrl/page/$title")
if (response.httpCode != null) {
logger.info("页面($title)不存在")
message.reply("页面($title)不存在")
return
}
if (response.redirectTarget != null) {
logger.info("重定向Wiki页面到: ${response.redirectTarget}")
response = Http.get<MCWikiPageResponse>(response.redirectTarget)
}
val messages = mutableListOf<MessageChain>()
val headerMsg = listOf("标题${response.title}的内容如下").asMessageChain()
val footerMsg = listOf(
"数据来源: https://zh.minecraft.wiki",
"协议: ${response.license.title}(${response.license.url})"
).asMessageChain(true)
val bodyMsg = MessageChain.Builder()
.addText(response.source.convertToHTML().extractPlainTextFromHtml())
.build()
messages.add(headerMsg)
messages.add(bodyMsg)
messages.add(footerMsg)
message.reply(messages.asNode(configManager.selfId))
}
}
}
}
@CommandDescription("帮助你寻找指定空置域大小并且在某个区块范围内的最多史莱姆区块范围")
class SlimeChunkHelperCommand : BaseCommand() {
override val commandNames = listOf("/slime")
private fun isSlimeChunk(worldSeed: Long, chunkX: Int, chunkZ: Int): Boolean {
val rng = Random(
worldSeed + (chunkX * chunkX) * 0x4C1906 +
(chunkX * 0x5AC0DB) + (chunkZ * chunkZ) * 0x4307A7L +
(chunkZ * 0x5F24F) xor 0x3AD8025FL
)
return rng.nextInt(10) == 0
}
private fun countSlimeChunksInRegion(xStart: Int, zStart: Int, chunkSize: Int, worldSeed: Long): Int {
var count = 0
for (x in xStart until xStart + chunkSize) {
for (z in zStart until zStart + chunkSize) {
if (isSlimeChunk(worldSeed, x, z)) {
count++
}
}
}
return count
}
private fun findMaxSlimeChunks(
worldSeed: Long,
searchRange: Int,
chunkSize: Int
): Pair<Pair<Pair<Int, Int>, Pair<Int, Int>>, Int> {
var maxCount = 0
var bestStartLocation = Pair(0, 0)
var bestEndLocation = Pair(0, 0)
for (x in -searchRange until searchRange - chunkSize + 1) {
for (z in -searchRange until searchRange - chunkSize + 1) {
val count = countSlimeChunksInRegion(x, z, chunkSize, worldSeed)
if (count > maxCount) {
maxCount = count
bestStartLocation = Pair(x, z)
bestEndLocation = Pair(x + chunkSize - 1, z + chunkSize - 1)
}
}
}
return Pair(Pair(bestStartLocation, bestEndLocation), maxCount)
}
private fun generateSlimeChunkImage(startX: Int, startZ: Int, endX: Int, endZ: Int, worldSeed: Long): String {
val width = endX - startX + 1
val height = endZ - startZ + 1
val pixelSize = 15
val imageWidth = width * pixelSize
val imageHeight = height * pixelSize
val image = BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB)
val graphics = image.graphics
val borderColor = Color.LIGHT_GRAY
for (x in startX..endX) {
for (z in startZ..endZ) {
val isSlime = isSlimeChunk(worldSeed, x, z)
val fillColor = if (isSlime) Color.PINK else Color.WHITE
val px = (x - startX) * pixelSize
val pz = (z - startZ) * pixelSize
graphics.color = fillColor
graphics.fillRect(px, pz, pixelSize, pixelSize)
graphics.color = borderColor
graphics.drawRect(px, pz, pixelSize, pixelSize)
}
}
graphics.dispose()
return image.toByteArray().encodeToBase64()
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
val msg = MessageChain.Builder()
.addText("此指令可以借助一个种子来帮助查找最适合史莱姆农场的位置")
.addNewLine()
.addText("如果不指定史莱姆农场的宽度的话默认为17x17区块大小")
.addNewLine()
.addText("不指定搜索区块范围默认为X: 500~-500, Y: 500~-500, 共计搜索100万个区块")
.addNewLine()
.addText("还可以指定空置域的大小, 不指定默认为17x17")
.addNewLine()
.addText("你可以使用`/slime <种子> [范围(例如600)] [空置域大小]`")
.build()
message.reply(msg)
return
}
val seed = args.first().toLong()
val slimeFarmSize = if (args.size == 1) 17 else args[1].toInt()
val range = if (args.size == 1) 500 else args[2].toInt()
val (location, slimeCount) = findMaxSlimeChunks(seed, range, slimeFarmSize)
val startX = location.first.first
val startZ = location.first.second
val endX = location.second.first
val endZ = location.second.second
val image = generateSlimeChunkImage(startX, startZ, endX, endZ, seed)
val msg = MessageChain.Builder()
.addText("起始区块坐标X:$startX Y:$startZ")
.addNewLine()
.addText("结束区块坐标X:$endX Y:$endZ")
.addNewLine()
.addText("共计史莱姆区块数量: $slimeCount")
.addImage(image, true)
.build()
message.reply(msg)
}
}
@CommandDescription("通过游戏名称或UUID来获取他的皮肤")
class MCSkinCommand : BaseCommand() {
override val commandNames = listOf("/skin")
private val uuidQueryUrl = "https://api.mojang.com/users/profiles/minecraft"
private val skinUrl = "https://sessionserver.mojang.com/session/minecraft/profile"
private fun String.isUsername(): Boolean {
val usernamePattern = Regex("^[a-zA-Z][a-zA-Z0-9_]{2,15}$")
return usernamePattern.matches(this)
}
/**
* 截取整张皮肤中的头部部分, 如果是单层皮肤则直接截取脸部
* 如果是双层皮肤也就将脸部作为背景图层, 将头发绘制在脸部上
* 代码来自
* `[YeeeesMOTD]
* (https://github.com/RTAkland/YeeeesMOTD/blob/main/core/src/main/kotlin/cn/rtast/yeeeesmotd/utils/SkinHeadUtil.kt)`
*/
private fun getSkinHead(skinUrl: String): String {
val url = URI(skinUrl).toURL()
val image = ImageIO.read(url)
var subImage = image.getSubimage(8, 8, 8, 8)
if (subImage.isFullyTransparent()) {
subImage = image.getSubimage(40, 8, 8, 8)
} else {
val hairLayer = image.getSubimage(40, 8, 8, 8)
val combined = BufferedImage(subImage.width, subImage.height, BufferedImage.TYPE_INT_ARGB)
val g2d = combined.createGraphics()
g2d.drawImage(subImage, 0, 0, null)
g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)
g2d.drawImage(hairLayer, 0, 0, null)
g2d.dispose()
subImage = combined
}
val zoom = subImage.scaleImage(128 to 128)
return zoom.toByteArray().encodeToBase64()
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("发送`/skin <uuid/游戏名称>`即可查询他的皮肤~ (仅限Java版)")
return
}
try {
val nameOrUUID = args.first().trim()
val uuid = if (nameOrUUID.isUsername()) Http.get<Username>("$uuidQueryUrl/$nameOrUUID").id
else nameOrUUID
val skinUrl = Http.get<Skin>("$skinUrl/$uuid")
.properties.first().value.decodeToString().fromJson<DecodedSkin>()
.textures.skin.url
val headBase64 = this.getSkinHead(skinUrl)
val msg = MessageChain.Builder()
.addImage(headBase64, true)
.addText(skinUrl)
.build()
message.reply(msg)
} catch (_: Exception) {
message.reply("输入错误, 检查一下是否输入正确吧~")
}
}
}
@CommandDescription("PingMC服务器")
class MCPingCommand : BaseCommand() {
override val commandNames = listOf("/mcping")
private fun String.removeColorCodes(): String {
return this.replace(Regex("§."), "")
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
val msg = MessageChain.Builder()
.addText("使用方法: /mcping <host>:[port] [java|be]")
.addNewLine()
.addText("不添加平台默认Java版~")
.build()
message.reply(msg)
return
}
val platform = if (args.size == 2) args.last() else "java"
when (platform) {
"java", "je", "Java", "JAVA" -> {
val parts = args.first().split(":")
var port = if (parts.size == 1) 25565 else parts.last().toInt()
var host = parts.first()
val srv = NslookupCommand.srv(host)
if (srv.isNotEmpty()) {
val srv = srv.first().split(" ")
port = srv[2].toInt()
host = srv.last()
}
val response = JavaPing().ping(host, port, 10000)
if (response == null) {
message.reply("无法获取服务器的消息请检查输入是否正确 >>> $host:$port")
return
}
val msg = MessageChain.Builder()
response.favicon?.let { msg.addImage(response.favicon!!.replace("data:image/png;base64,", ""), true) }
msg.addText("服务器地址: $host:$port")
.addNewLine()
.addText("服务器类型: Java | 延迟: ${response.latency}ms")
.addNewLine()
.addText("服务器版本: ${response.version.name}/${response.version.protocol}")
.addNewLine()
.addText("在线玩家数: ${response.players.online}/${response.players.max}")
message.reply(msg.build())
}
"be", "bedrock", "BE", "Bedrock" -> {
val parts = args.first().split(":")
val port = if (parts.size == 1) 19132 else parts.last().toInt()
val host = parts.first()
try {
val response = BedrockPing().ping(host, port, 10000)
val msg = MessageChain.Builder()
.addText("服务器地址: $host:$port")
.addNewLine()
.addText("服务器类型: Bedrock(基岩版) | 延迟: ${response.latency}ms")
.addNewLine()
.addText("服务器版本: ${response.version}/${response.protocolVersion}")
.addNewLine()
.addText("在线玩家: ${response.onlinePlayers}/${response.maxPlayers}")
.addNewLine()
.addText("MOTD: ${response.motd.removeColorCodes()}")
.addNewLine()
.addText(response.subTitle.removeColorCodes())
message.reply(msg.build())
} catch (_: SocketException) {
message.reply("无法从服务器接收Ping结果请检查服务器地址是否正确 >>> $host:$port")
}
}
else -> {
message.reply("输入错误请在 `java` 和 `be`中选择 >>> $platform")
}
}
}
}
@CommandDescription("获取MC账号的AccessToken")
class MCLoginCommand : BaseCommand() {
override val commandNames = listOf("/mclogin")
private val deviceCodeUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
private val redeemCodeUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
private val xboxLiveAuthUrl = "https://user.auth.xboxlive.com/user/authenticate"
private val obtainXSTSUrl = "https://xsts.auth.xboxlive.com/xsts/authorize"
private val clientId = configManager.azureAppClientId
private val waitingList = mutableListOf<Long>()
private val deviceCodeMap = mutableMapOf<Long, String>()
/**
* 获取user code 和device code 和验证url
*/
private fun getDeviceCode(): Triple<String, String, String> {
val form = FormBody.Builder()
.add("client_id", clientId)
.add("scope", "XboxLive.signin")
.build()
val response = Http.post<DeviceCodeResponse>(deviceCodeUrl, form)
return Triple(response.userCode, response.deviceCode, response.verificationUri)
}
/**
* 将这个code兑换成一个access token
*/
private fun redeemCode(deviceCode: String): String {
val form = FormBody.Builder()
.add("client_id", clientId)
.add("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
.add("device_code", deviceCode)
.build()
val response = Http.post<RedeemCodeResponse>(redeemCodeUrl, form)
return response.accessToken
}
/**
* 登录Xbox
*/
private fun loginXboxLive(accessToken: String): Pair<String, String> {
val payload = LoginXboxLivePayload(properties = LoginXboxLivePayload.Properties("d=$accessToken"))
val response = Http.post<LoginXboxLiveResponse>(xboxLiveAuthUrl, payload.toJson())
val token = response.token
val uhs = response.displayClaims.xui.first().uhs
return token to uhs
}
/**
* 获取最后的mc access token, 这个access token可以直接用于登录游戏
*/
private fun obtainXSTSToken(token: String, uhs: String): String {
val payload = ObtainXSTSPayload(properties = ObtainXSTSPayload.Properties(listOf(token)))
val response = Http.post<ObtainXSTSResponse>(obtainXSTSUrl, payload.toJson())
return response.token
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("这个命令可以获取你的MC登录Token发送 `/mclogin login`进行操作吧")
return
}
deleteAccessTokenById(message.sender.userId)
val action = args.first()
if (action == "确认" || action == "confirm") {
if (message.sender.userId !in waitingList) {
message.reply("你还没开始登陆呢")
} else {
waitingList.removeIf { it == message.sender.userId }
val deviceCode = deviceCodeMap[message.sender.userId]!!
deviceCodeMap.remove(message.sender.userId)
val accessToken = this.redeemCode(deviceCode)
val (token, uhs) = this.loginXboxLive(accessToken)
val minecraftAccessToken = this.obtainXSTSToken(token, uhs)
insertNewAccessToken(minecraftAccessToken, message.sender.userId)
message.reply("你的账号token已经被保存到了数据库中, 请加机器人好友然后发送`/mclogin`来获取这个Token, Token会在你获取之后从数据库中删除")
}
} else {
if (message.sender.userId !in waitingList) {
waitingList.add(message.sender.userId)
val deviceInfo = getDeviceCode()
val msg = MessageChain.Builder()
.addText("你的用户代码是: ${deviceInfo.first}")
.addNewLine()
.addText("请前往: ${deviceInfo.third}进行验证")
.addNewLine()
.addText("登陆完成后发送`/mclogin confirm` 或 `/mclogin 确认`")
.build()
deviceCodeMap[message.sender.userId] = deviceInfo.second
message.reply(msg)
} else {
waitingList.removeIf { it == message.sender.userId }
deviceCodeMap.remove(message.sender.userId)
message.reply("回复错误已结束本次登录流程")
}
}
}
override suspend fun executePrivate(listener: OneBotListener, message: PrivateMessage, args: List<String>) {
val token = getAccessTokenById(message.sender.userId)
if (token == null) {
message.reply("你还没登录呢!")
} else {
val msg = MessageChain.Builder()
.addText("你的AccessToken如下: ")
.addNewLine()
.addText(token.token)
.addNewLine()
.addText("这个Token是敏感信息请妥善保管")
.build()
deleteAccessTokenById(message.sender.userId)
message.reply(msg)
}
}
}
@CommandDescription("通过RCON来控制MC服务器(仅限私聊执行)")
class RCONCommand : BaseCommand() {
override val commandNames = listOf("/rcon", "/r")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
message.reply("请在私聊中使用此命令")
}
override suspend fun executePrivate(listener: OneBotListener, message: PrivateMessage, args: List<String>) {
if (args.isEmpty()) {
val msg = MessageChain.Builder()
.addText("这条指令用于在群内操作mc服务器")
.addNewLine()
.addText("使用`/r add <name> <host> <port> <password>` 可以创建一条配置")
.addNewLine()
.addText("使用`/r <name> <command>`可以执行命令")
.addNewLine()
.addText("使用`/r remove <name>`可以移除这条RCON配置")
.build()
message.reply(msg)
return
}
val action = args.first()
when (action) {
"get" -> {
val allRcons = rconManager.getAllRconsById(message.sender.userId)
if (allRcons.isEmpty()) {
message.reply("你还没有RCON配置呢, 发送`/r add <name> <host> <port> <password>`来创建一个配置吧")
return
}
val messages = mutableListOf<MessageChain>()
.also { it.add(listOf("所有配置如下").asMessageChain()) }
allRcons.forEach {
val msg = MessageChain.Builder()
.addText("名称: ${it.name}")
.addNewLine()
.addText("地址: ${it.host}:${it.port}")
.addNewLine()
.addText("密码: *********")
.build()
messages.add(msg)
}
message.reply(messages.asNode(configManager.selfId))
}
"add" -> {
val name = args[1]
val host = args[2]
val port = args[3].toInt()
val password = args[4]
rconManager.insertConfig(message.sender.userId, name, host, port, password)
message.reply("成功增加一条记录: $name")
}
"remove" -> {
val name = args.last()
val count = rconManager.removeRRCON(message.sender.userId, name)
if (count == 0) {
message.reply("RCON($name)不存在")
} else {
message.reply("成功移除了$name")
}
}
else -> {
val name = args.first().trim()
val command = args.drop(1).joinToString(" ").trim()
val rconData = rconManager.getRcon(message.sender.userId, name)
if (rconData == null) {
message.reply("RCON($name)配置不存在")
} else {
try {
val result = rconManager.executeCommand(message.sender.userId, name, command)
val messages = mutableListOf<MessageChain>()
.also { it.add(listOf("执行结果如下").asMessageChain()) }
.also { it.add(listOf(result).asMessageChain()) }
.asNode(configManager.selfId)
message.reply(messages)
} catch (_: AuthFailedException) {
message.reply("执行失败, 密码不正确")
}
}
}
}
}
}
@CommandDescription("Java版服务器假人压测")
class MCBotCommand : BaseCommand() {
override val commandNames = listOf("/mcbot")
private val userClientMap = mutableMapOf<Long, MutableList<MCClient>>()
private fun getPlayerList(host: String, port: Int): Pair<Int, Int> {
val response = JavaPing().ping(host, port, 8000)!!
return response.players.max to response.players.online
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val clients = mutableListOf<MCClient>()
val action = args.first()
when (action) {
"start" -> {
if (userClientMap.containsKey(message.sender.userId)) {
message.reply("你有一个正在进行的任务不能开启另外一个任务请先使用`/mcbot cancel`取消任务")
return
}
val address = args[1]
val host = address.split(":").first()
val port = address.split(":").last().toInt()
val count = args[2].toInt()
val executor = Executors.newFixedThreadPool(count)
val (maxPlayer, onlinePlayer) = this.getPlayerList(host, port)
val availableSit = maxPlayer - onlinePlayer
if (count > availableSit) {
message.replyAsync("当前服务器仅剩余${availableSit}个玩家可进入!!")
}
(1..count).forEach {
val client = MCClient(host, port, generateRandomString())
.also { it.createClient() }
executor.execute { clients.add(client.runBot()) }
}
userClientMap[message.sender.userId] = clients
message.reply("成功开启任务: $address, $count")
}
"cancel" -> {
if (userClientMap.containsKey(message.sender.userId)) {
userClientMap.entries.forEach {
if (it.key == message.sender.userId) {
it.value.forEach { it.disconnect() }
userClientMap.remove(it.key)
}
}
message.reply("成功取消任务")
} else {
message.reply("你还没开启任务呢")
}
}
"message" -> {
val content = args.drop(1).joinToString(" ").trim()
userClientMap[message.sender.userId]!!.forEach { it.sendChat(content) }
}
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/14
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.API_RTAST_URL
import cn.rtast.fancybot.PBI_API_URL
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.pastebin.PastebinPayload
import cn.rtast.fancybot.entity.pastebin.PastebinResponse
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("Pastebin!")
class PastebinCommand : BaseCommand() {
override val commandNames = listOf("/pastebin", "/pb")
companion object {
fun createPastebin(content: String): String {
val pastebinPayload = PastebinPayload(content)
val response = Http.post<PastebinResponse>(
"$API_RTAST_URL/api/pastebin", pastebinPayload.toJson(),
params = mapOf("key" to configManager.apiRtastKey)
)
return "$PBI_API_URL/-/${response.id}"
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("使用`/pb <内容>`或者回复一个消息`/pb`可以快速创建一个pastebin")
return
}
val content = args.joinToString(" ")
message.reply(createPastebin(content))
insertActionRecord(CommandAction.Pastebin, message.sender.userId, content)
}
}
@@ -0,0 +1,96 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/8/30
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.client.j2se.BufferedImageLuminanceSource
import com.google.zxing.client.j2se.MatrixToImageWriter
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeWriter
import java.awt.image.BufferedImage
import java.net.URI
@CommandDescription("生成二维码")
class GenerateQRCodeCommand : BaseCommand() {
override val commandNames = listOf("/qr")
private fun generateQRCode(content: String): String {
val qrCodeWriter = QRCodeWriter()
val hints = mapOf(EncodeHintType.CHARACTER_SET to "UTF-8")
val bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints)
return MatrixToImageWriter.toBufferedImage(bitMatrix).toByteArray().encodeToBase64()
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("发送`/qr <内容>`即可生成一张二维码的图片")
return
}
val content = args.joinToString(" ")
val image = this.generateQRCode(content)
val msg = MessageChain.Builder()
.addImage(image, true)
.build()
message.reply(msg)
insertActionRecord(CommandAction.GenQRCode, message.sender.userId)
}
}
@CommandDescription("解析二维码中的内容!")
class ScanQRCodeCommand : BaseCommand() {
override val commandNames = listOf("/scan")
companion object {
private val waitingList = mutableListOf<Long>()
private fun scanQRCode(image: BufferedImage): String {
val luminanceSource = BufferedImageLuminanceSource(image)
val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource))
val result = MultiFormatReader().decode(binaryBitmap)
return result.text
}
suspend fun callback(message: GroupMessage) {
if (message.sender() in waitingList) {
waitingList.remove(message.sender())
if (!message.message.any { it.type == ArrayMessageType.image }) {
message.reply("回复错误已取消本次操作")
return
}
val image = message.message.find { it.type == ArrayMessageType.image }?.data?.file!!
val bufferedImage = URI(image).toURL().readBytes().toBufferedImage()
val result = this.scanQRCode(bufferedImage)
message.reply("二维码扫描结果: $result")
}
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (message.sender() !in waitingList) {
waitingList.add(message.sender())
message.reply("继续发送一张二维码图片")
} else {
message.reply("回复错误已经取消本次操作")
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/3
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.music.TopSong
import cn.rtast.fancybot.util.Http
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.MusicShareType
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("获取随机热榜音乐(网易云)")
class RandomMusicCommand : BaseCommand() {
override val commandNames = listOf("随机音乐", "随机歌曲")
private val ncmApiUrl = "${configManager.ncmAPI}/top/song"
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val id = Http.get<TopSong>(ncmApiUrl).data.random().privilege.id
val msg = MessageChain.Builder()
.addMusicShare(MusicShareType.Netease, id.toString())
.build()
listener.sendGroupMessage(message.groupId, msg)
}
}
@@ -0,0 +1,34 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/1
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.QQFace
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("让你的消息得到回应(reaction)!")
class ReactionCommand : BaseCommand() {
override val commandNames = listOf("reaction", "回应")
companion object {
suspend fun reaction(listener: OneBotListener, message: GroupMessage, messageId: Long, limit: Int = 30) {
(0..limit - 1).forEach {
listener.reaction(
message.groupId,
messageId,
QQFace.entries.random().id.toString()
)
}
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) =
reaction(listener, message, message.messageId)
}
@@ -8,8 +8,8 @@
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.entity.enums.CountryList
import cn.rtast.fancybot.util.randomBooleanWithProbability
import cn.rtast.fancybot.enums.CountryList
import cn.rtast.fancybot.util.misc.randomBooleanWithProbability
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -0,0 +1,14 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/15
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
@CommandDescription("复读鸡")
object RepeatCommand {
}
@@ -14,7 +14,7 @@ import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("资料卡点赞")
class LikeMeCommand : BaseCommand() {
class SendLikeCommand : BaseCommand() {
override val commandNames = listOf("赞我")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
@@ -9,9 +9,10 @@ package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.enums.CommandAction
import cn.rtast.fancybot.util.file.insertActionRecord
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import org.simplejavamail.api.mailer.config.TransportStrategy
import org.simplejavamail.email.EmailBuilder
@@ -47,16 +48,13 @@ class SendMailCommand : BaseCommand() {
message.reply("发送`/mail <收件人地址>`即可发送测试邮件~")
return
}
if (message.sender.userId !in configManager.admins) {
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("你不许发邮件")
.build()
listener.sendGroupMessage(message.groupId, msg)
return
}
if (message.sender.isAdmin || message.sender.isOwner) {
val target = args.first()
this.sendMail(target)
message.reply("正在发送邮件, 请查收~(可能会出现在垃圾桶中~)")
insertActionRecord(CommandAction.SendMail, message.sender.userId, target)
} else {
message.reply("你不许发邮件")
}
}
}
@@ -7,10 +7,13 @@
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.API_RTAST_URL
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.entity.ShortLink
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.shortlink.ShortLinkPayload
import cn.rtast.fancybot.entity.shortlink.ShortLinkResponse
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
@@ -19,18 +22,25 @@ import cn.rtast.rob.util.ob.OneBotListener
class ShortLinkCommand : BaseCommand() {
override val commandNames = listOf("/s")
private val shortLinkApi = "https://api.rtast.cn"
companion object {
fun String.makeShortLink(): String {
val shortLink = Http.post<ShortLinkResponse>(
"$API_RTAST_URL/api/sl",
ShortLinkPayload(this).toJson(),
params = mapOf("key" to configManager.apiRtastKey)
)
return "$API_RTAST_URL/${shortLink.id}"
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("发送`/s <url>`来生成短链接`不要放入一些奇怪的链接哦~")
return
}
val target = args.first().encodeToBase64()
val shortLink = Http.post<ShortLink>(
"$shortLinkApi/short_link",
params = mapOf("target" to target)
)
message.reply("$shortLinkApi/s?rnd_id=${shortLink.id}")
val target = args.first()
val shortLink = target.makeShortLink()
message.reply(shortLink)
}
}
@@ -0,0 +1,43 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/7
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.entity.tts.TTSResponse
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.str.proxy
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import okhttp3.FormBody
@CommandDescription("将文字转换成语音(TTS)!")
class TTSCommand : BaseCommand() {
override val commandNames = listOf("/tts")
private val ttsApiUrl = "https://ttsmp3.com/makemp3_new.php"
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("发送`/tts <文字>`就可以生成语音啦~")
return
}
val content = args.joinToString(" ").trim()
val form = FormBody.Builder()
.add("msg", content)
.add("lang", "Zhiyu")
.add("source", "ttsmp3")
.build()
val response = Http.post<TTSResponse>(ttsApiUrl, form)
val msg = MessageChain.Builder()
.addRecord(response.url.proxy)
.build()
listener.sendGroupMessage(message.groupId, msg)
}
}
@@ -7,11 +7,14 @@
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@Deprecated("已迁移到兑换列表中")
@CommandDescription("获得十张色图!")
class TenSetuCommand : BaseCommand() {
override val commandNames = listOf("十张色图")
@@ -0,0 +1,69 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
import kotlin.random.Random
@CommandDescription("今天吃什么")
class TodayEatCommand : BaseCommand() {
override val commandNames = listOf("今天吃什么")
private val foodList = listOf(
"炒饭", "火锅", "汉堡", "披萨", "寿司", "拉面", "炸鸡", "米粉",
"牛排", "烤串", "沙拉", "饺子", "意大利面", "烤鸭", "豆腐脑",
"蛋糕", "烧烤", "咖喱饭", "麻辣烫", "烤鱼", "牛肉面", "凉皮",
"西餐", "日式便当", "韩式拌饭", "生煎包", "炸酱面", "煎饼果子",
"酸辣粉", "煲仔饭", "", ""
)
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (KFCCommand.isThursday() && Random.nextBoolean()) {
message.reply("今天是疯! 狂! 星! 期! 四!, 所以吃肯德基!")
return
}
message.reply("今天吃${foodList.random()}吧~")
}
}
@CommandDescription("今天不吃什么")
class TodayDontEatCommand : BaseCommand() {
override val commandNames = listOf("今天不吃什么")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
message.reply("你吃个结巴, 是吧")
}
}
@CommandDescription("今天喝什么")
class TodayDrinkCommand : BaseCommand() {
override val commandNames = listOf("今天喝什么")
private val drinkList = listOf(
"咖啡", "绿茶", "奶茶", "橙汁", "苹果汁", "可乐", "啤酒", "矿泉水",
"苏打水", "红茶", "酸梅汤", "豆浆", "椰子水", "冰沙", "葡萄汁",
"柠檬水", "奶昔", "热巧克力", "果茶", "乌龙茶", "茉莉花茶", "白开水",
"气泡水", "奶盖茶", "冰可乐", "草莓汁", "芒果汁", "蓝莓汁", "热奶茶", "蜂蜜柚子茶"
)
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
message.reply("今天喝${drinkList.random()}吧~")
}
}
@CommandDescription("今天不吃什么")
class TodayDontDrinkCommand : BaseCommand() {
override val commandNames = listOf("今天不喝什么")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
message.reply("你喝个结巴, 是吧")
}
}
@@ -0,0 +1,91 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/5
*/
package cn.rtast.fancybot.commands.misc
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.util.str.decodeToString
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("将人类的语言翻译成尊嘟假嘟语还能将尊嘟假嘟语翻译成人类的语言!")
class ZDJDCommand : BaseCommand() {
override val commandNames = listOf("尊嘟假嘟", "zdjd")
private val b64 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/="
private val leftEye = listOf("o", "0", "O", "Ö")
private val mouse = listOf("w", "v", ".", "_")
private val rightEye = listOf("o", "0", "O", "Ö")
private val table = mutableListOf<String>()
private val separator = " "
init {
makeTable()
}
private fun makeTable() {
for (i in leftEye.indices) {
for (j in mouse.indices) {
for (k in rightEye.indices) {
table.add(leftEye[i] + mouse[j] + rightEye[k])
}
}
}
}
private fun human2zdjd(text: String): String {
val encoded = text.encodeToBase64()
val arr = mutableListOf<String>()
for (c in encoded) {
if (c != '=') {
val n = b64.indexOf(c)
arr.add(table[n])
}
}
return arr.joinToString(separator)
}
private fun zdjd2human(t: String): String {
val arr = t.split(separator)
val resultArr = mutableListOf<String>()
for (c in arr) {
if (c.isEmpty()) continue
val n = table.indexOf(c)
if (n < 0) throw IllegalArgumentException("Invalid zdjd code")
resultArr.add(b64[n].toString())
}
var base64String = resultArr.joinToString("")
val padding = resultArr.size % 4
if (padding > 0) {
base64String += "=".repeat(4 - padding)
}
return base64String.decodeToString()
}
private fun String.isZdjd(): Boolean {
return try {
zdjd2human(this)
true
} catch (_: Exception) {
false
}
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
val text = if (message.message.any { it.type == ArrayMessageType.image }) {
message.message.find { it.type == ArrayMessageType.image }!!.data.file!!
} else args.joinToString(" ")
if (text.isZdjd()) {
message.reply(zdjd2human(text))
} else {
message.reply(human2zdjd(text))
}
}
}
@@ -11,7 +11,7 @@ import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.commands
import cn.rtast.fancybot.niuziBankManager
import cn.rtast.fancybot.niuziManager
import cn.rtast.fancybot.util.getUserName
import cn.rtast.fancybot.util.misc.getUserName
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
@@ -0,0 +1,68 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.commands.niuzi
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.niuziBankManager
import cn.rtast.fancybot.niuziManager
import cn.rtast.fancybot.util.misc.getUserName
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
@CommandDescription("在群内设置某个成员的牛子银行余额或者现金牛子长度")
class NiuziManagerCommand : BaseCommand() {
override val commandNames = listOf("管理牛子")
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
message.reply("此命令可以管理成员的牛子或者银行账户")
return
}
if (message.sender.isAdmin || message.sender.isOwner) {
val target = message.message.find { it.type == ArrayMessageType.at }!!.data.qq!!.toLong()
val method = args[1]
val amount = args.last().toDouble()
when (method) {
"set", "s", "设置" -> {
val type = args[2]
when (type) {
"yh", "银行" -> {
val before = niuziBankManager.getUser(target)
niuziBankManager.deposit(target, amount, listener.getUserName(message.groupId, target))
val after = niuziBankManager.getUser(target)
val msg = MessageChain.Builder()
.addText("设置成功! 金额: $amount")
.addNewLine()
.addText("设置前余额: ${before?.balance?.toFloat()} | 设置后余额: ${after?.balance?.toFloat()}")
.build()
message.reply(msg)
}
else -> {
val before = niuziManager.getUser(target)
niuziManager.updateLength(target, amount)
val after = niuziManager.getUser(target)
val msg = MessageChain.Builder()
.addText("设置成功! 金额: $amount")
.addNewLine()
.addText("设置前长度: ${before?.length?.toFloat()} | 设置后长度: ${after?.length?.toFloat()}")
.build()
message.reply(msg)
}
}
}
}
} else {
message.reply("你没权限呢!")
}
}
}
@@ -22,14 +22,13 @@ class NiuziRedeemCommand : BaseCommand() {
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (args.isEmpty()) {
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("发送 /rdm <物品名称> 即可兑换哦!")
.addNewLine()
.addText("发送 /rdm list 即可查看所有可兑换的物品~")
.addNewLine()
.addText("发送 `我的牛子` 可以查看自己的牛子长度~")
.build()
listener.sendGroupMessage(message.groupId, msg)
message.reply(msg)
return
}
val status = niuziManager.getUser(message.sender.userId)
@@ -47,29 +46,26 @@ class NiuziRedeemCommand : BaseCommand() {
val selectedItem = itemManager.items.find { it.itemNames.any { any -> any == item } }
if (selectedItem == null) {
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("没有找到你想兑换的东西呢")
.addNewLine()
.addText("你可以发送 /rdm list来查看所有可兑换的物品")
.build()
listener.sendGroupMessage(message.groupId, msg)
message.reply(msg)
return
}
val selectedItemPrice = selectedItem.itemPrice
if (selectedItemPrice > status.length) {
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("你的牛子长度不够兑换这个物品呢~")
.addNewLine()
.addText("你有: ${status.length}cm, 兑换需要: $selectedItemPrice cm~\"")
.build()
listener.sendGroupMessage(message.groupId, msg)
message.reply(msg)
return
}
val afterStatus = niuziManager.redeemItem(message.sender.userId, selectedItemPrice)?.length!!
val action = selectedItem.redeemInGroup(listener, message, afterStatus)
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("兑换成功! 你花费了${selectedItemPrice}cm来兑换奖品, 还剩${afterStatus}cm")
.addNewLine()
.addText("-------------------")
@@ -9,7 +9,7 @@ package cn.rtast.fancybot.commands.niuzi
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.niuziManager
import cn.rtast.fancybot.util.getUserName
import cn.rtast.fancybot.util.misc.getUserName
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
@@ -8,14 +8,20 @@
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.util.makeGif
import cn.rtast.fancybot.commands.misc.ShortLinkCommand.Companion.makeShortLink
import cn.rtast.fancybot.util.Logger
import cn.rtast.fancybot.util.file.getFileType
import cn.rtast.fancybot.util.misc.ImageBed
import cn.rtast.fancybot.util.misc.makeGif
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.toByteArray
import cn.rtast.rob.entity.GetMessage
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import cn.rtast.rob.util.ob.asMessageChain
import com.madgag.gif.fmsware.GifDecoder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -33,6 +39,7 @@ class AsciiArtCommand : BaseCommand() {
companion object {
private const val FONT_SIZE = 12
private val font = Font("Monospaced", Font.PLAIN, FONT_SIZE)
private val logger = Logger.getLogger<AsciiArtCommand>()
val waitingList = mutableListOf<Long>()
private fun BufferedImage.convertToAscii(): String {
@@ -86,48 +93,94 @@ class AsciiArtCommand : BaseCommand() {
return img
}
suspend fun callback(message: GroupMessage) {
if (message.sender.userId !in waitingList) return
waitingList.removeIf { it == message.sender.userId }
if (message.message.any { it.type == ArrayMessageType.image }
|| message.message.any { it.type == ArrayMessageType.mface }) {
val url = if (message.message.any { it.type == ArrayMessageType.image })
message.message.find { it.type == ArrayMessageType.image }!!.data.file!!
else message.message.find { it.type == ArrayMessageType.mface }!!.data.url!!
suspend fun generateAsciiArt(url: String): MessageChain {
val gifStream = withContext(Dispatchers.IO) { URI(url).toURL().openStream() }
val decoder = GifDecoder()
try {
decoder.read(gifStream)
if (decoder.frameCount == 0) {
val bufferedImage = withContext(Dispatchers.IO) { ImageIO.read(URI(url).toURL()) }
val imageBase64 = bufferedImage.convertToAscii()
val imageBytes = bufferedImage.convertToAscii()
.saveAsciiArtToImage(bufferedImage.width, bufferedImage.height)
.toByteArray().encodeToBase64()
val msg = MessageChain.Builder().addImage(imageBase64, true).build()
message.reply(msg)
.toByteArray()
val imageBedUrl = ImageBed.upload(imageBytes, imageBytes.getFileType()).makeShortLink()
val msg = MessageChain.Builder()
.addImage(imageBytes.encodeToBase64(), true)
.addText(imageBedUrl)
.build()
return msg
} else {
println("Making gif ascii art, frames: ${decoder.frameCount}")
logger.info("制作GIF Ascii art, 总帧数: ${decoder.frameCount}")
val frames = (0 until decoder.frameCount).map { decoder.getFrame(it) }
val asciiFrames = mutableListOf<BufferedImage>()
val width = frames.first().width
val height = frames.first().height
frames.forEach { asciiFrames.add(it.convertToAscii().saveAsciiArtToImage(width, height)) }
frames.forEachIndexed { index, item ->
asciiFrames.add(item.convertToAscii().saveAsciiArtToImage(width, height))
logger.info("帧:${index}处理完成")
}
logger.info("合成GIF中...")
val gifBytes = decoder.makeGif(asciiFrames)
val imageBedUrl = ImageBed.upload(gifBytes, gifBytes.getFileType()).makeShortLink()
logger.info("合并完成")
val gifBase64 = gifBytes.encodeToBase64()
message.reply(MessageChain.Builder().addImage(gifBase64, true).build())
logger.info("处理后的图片大小: ${(gifBytes.size / 1024 / 1024).toInt()}MB")
val msg = MessageChain.Builder()
.addImage(gifBase64, true)
.addText(imageBedUrl)
.build()
return msg
}
} catch (_: Exception) {
message.reply("处理GIF失败")
} catch (_: OutOfMemoryError) {
return listOf("GIF处理失败: 内存溢出").asMessageChain()
logger.info("GIF处理失败: 内存溢出")
} catch (e: Exception) {
return listOf("处理GIF失败: ${e.message}").asMessageChain()
logger.info("gif处理失败: ${e.message}")
}
}
suspend fun callback(message: GroupMessage) {
if (message.sender.userId !in waitingList) return
waitingList.removeIf { it == message.sender.userId }
if (message.message.any { it.type == ArrayMessageType.image }
|| message.message.any { it.type == ArrayMessageType.mface }) {
val url = this.getImageUrl(message)
val msg = this.generateAsciiArt(url)
message.reply(msg)
} else {
message.reply("回复错误已取消本次操作")
}
}
fun getImageUrl(groupMessage: GroupMessage): String {
return getImageUrlFromUnified(toUnifiedMessage(groupMessage))
}
fun getImageUrl(getMessageData: GetMessage.Data): String {
return getImageUrlFromUnified(toUnifiedMessage(getMessageData))
}
private fun getImageUrlFromUnified(messages: List<UnifiedMessage>): String {
return if (messages.any { it.type == ArrayMessageType.image })
messages.find { it.type == ArrayMessageType.image }!!.file!!
else messages.find { it.type == ArrayMessageType.mface }!!.url!!
}
private fun toUnifiedMessage(groupMessage: GroupMessage): List<UnifiedMessage> {
return groupMessage.message.map { UnifiedMessage(it.type, it.data.file, it.data.url) }
}
private fun toUnifiedMessage(getMessageData: GetMessage.Data): List<UnifiedMessage> {
return getMessageData.message.map { UnifiedMessage(it.type, it.data.file, it.data.url) }
}
private data class UnifiedMessage(val type: ArrayMessageType, val file: String?, val url: String?)
}
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
if (message.sender.userId !in waitingList) {
message.reply("请继续发送一张图片如果输入错误则取消本次操作")
message.reply("请继续发送一张图片, 如果输入错误则取消本次操作, 如果GIF帧数过多可能会需要很长时间(也可能处理失败)")
waitingList.add(message.sender.userId)
return
} else {
@@ -1,201 +0,0 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/9/6
*/
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.entity.bili.*
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.Resources
import cn.rtast.fancybot.util.drawCustomImage
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.formatNumber
import cn.rtast.fancybot.util.str.fromJson
import cn.rtast.fancybot.util.str.setTruncate
import cn.rtast.fancybot.util.toByteArray
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import okhttp3.OkHttpClient
import okhttp3.Request
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.net.URI
import javax.imageio.ImageIO
object BVParseCommand {
private const val VIDEO_STAT_URL = "https://api.bilibili.com/x/web-interface/view"
private const val USER_STAT_URL = "https://api.bilibili.com/x/relation/stat"
private const val SHORT_URL_API_URL = "https://api.bilibili.com/x/share/click"
private const val VIEW_COUNT_URL = "https://api.bilibili.com/x/player/online/total"
private const val CANVAS_WIDTH = 1000
private const val CANVAS_HEIGHT = 600
private val backgroundColor = Color(43, 43, 43)
private val upBarColor = Color(102, 102, 102)
private val textColor = Color.WHITE
private val titleFont = Font("Serif", Font.ITALIC, 40)
private val numberFont = Font("Serif", Font.ITALIC, 25)
private val twoTwoLogo = ImageIO.read(Resources.loadFromResources("bili/22-coin.png"))
private val likeIcon = ImageIO.read(Resources.loadFromResources("bili/like.png"))
private val coinIcon = ImageIO.read(Resources.loadFromResources("bili/coin.png"))
private val viewIcon = ImageIO.read(Resources.loadFromResources("bili/view.png"))
private val shareIcon = ImageIO.read(Resources.loadFromResources("bili/share.png"))
private val favoriteIcon = ImageIO.read(Resources.loadFromResources("bili/favorite.png"))
private val replyIcon = ImageIO.read(Resources.loadFromResources("bili/reply.png"))
private val tempOkHttpClient = OkHttpClient()
private fun generateShortUrl(bvid: String, oid: Long): String {
val result = Http.post<ShortUrl>(
SHORT_URL_API_URL, mapOf(
"buvid" to bvid,
"build" to 6114514,
"platform" to "unix",
"share_channel" to "COPY",
"share_mode" to 2,
"share_id" to "main.ugc-video-detail.0.0.pv",
"oid" to oid
)
)
return result.data.content.split(" ").last()
}
private fun getViewCount(bvid: String, cid: Long): String {
return Http.get<ViewCount>(
VIEW_COUNT_URL, mapOf(
"bvid" to bvid,
"cid" to cid
)
).data.total
}
private fun getVideoStat(bvid: String): VideoStat {
return Http.get<VideoStat>(VIDEO_STAT_URL, mapOf("bvid" to bvid))
}
private fun createResponseImage(
title: String,
author: String,
authorFace: String,
picUrl: String,
reply: String,
view: String,
coin: String,
share: String,
like: String,
favorite: String,
fans: Int,
viewCount: String,
): String {
val coverImage = ImageIO.read(URI(picUrl).toURL())
val faceImage = ImageIO.read(URI(authorFace).toURL())
val canvas = BufferedImage(CANVAS_WIDTH, CANVAS_HEIGHT, BufferedImage.TYPE_INT_RGB)
val g2d = canvas.createGraphics()
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.color = backgroundColor
g2d.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
// draw rect
g2d.color = upBarColor
g2d.fillRect(0, 0, CANVAS_WIDTH, 120)
g2d.color = textColor
// draw numbers
g2d.font = numberFont
g2d.drawString(like, 110, 200)
g2d.drawString(coin, 110, 290)
g2d.drawString(view, 110, 380)
g2d.drawString(favorite, 300, 380)
g2d.drawString(reply, 300, 290)
g2d.drawString(share, 300, 200)
// draw video title
val truncatedText = setTruncate(title, g2d)
g2d.font = titleFont
g2d.drawString(truncatedText, 20, 70)
// draw author name
g2d.drawString(author, 40, 580)
// draw icons
g2d.drawCustomImage(favoriteIcon, 230, 340, 50.0, 50.0, false)
g2d.drawCustomImage(replyIcon, 230, 250, 50.0, 50.0, false)
g2d.drawCustomImage(shareIcon, 230, 160, 50.0, 50.0, false)
g2d.drawCustomImage(viewIcon, 40, 340, 50.0, 50.0, false)
g2d.drawCustomImage(coinIcon, 40, 250, 50.0, 50.0, false)
g2d.drawCustomImage(likeIcon, 40, 160, 50.0, 50.0, false)
// draw 22 logo
g2d.drawCustomImage(twoTwoLogo, 800, 450, 90.0, 160.0, false)
// draw fans count
g2d.font = numberFont
g2d.drawString(fans.formatNumber() + "粉丝", 120, 520)
// draw viewing count
g2d.drawString("${viewCount}人正在观看", 40, 460)
// draw author face
g2d.drawCustomImage(faceImage, 40, 480, 60.0, 60.0, true)
// draw cover image
g2d.drawCustomImage(coverImage, 430, 140, 600.0, 300.0, true)
g2d.dispose()
return canvas.toByteArray().encodeToBase64()
}
fun getShortUrlBVID(shortUrl: String): String {
val request = Request.Builder().url(shortUrl.split(" ").last()).build()
val redirectedUrl = tempOkHttpClient.newCall(request).execute()
redirectedUrl.use {
return it.request.url.toString().split("/")[4].split("?").first()
}
}
suspend fun parse(listener: OneBotListener, message: GroupMessage) {
// parse bilibili video with a link, bvid or b23.tv link
val bvid = if (message.rawMessage.startsWith("BV")) {
message.rawMessage
} else if (message.rawMessage.split(" ").last().startsWith("https://b23.tv/")) {
val shortUrl = message.rawMessage.split(" ").last()
BVParseCommand.getShortUrlBVID(shortUrl)
} else if (message.message.find { it.type == ArrayMessageType.json } != null) {
val card = message.message.find { it.type == ArrayMessageType.json }!!
.data.data!!.toString().fromJson<CardShare>()
val shortUrl = if (card.meta.detail == null) {
if (!card.meta.news?.tag!!.contains("哔哩哔哩")) return
card.meta.news.jumpUrl
} else {
if (!card.meta.detail.title.contains("哔哩哔哩")) return
card.meta.detail.qqDocUrl
}
BVParseCommand.getShortUrlBVID(shortUrl)
} else {
message.rawMessage.split("?").first()
.split("/")
.last { it.isNotEmpty() && it.isNotBlank() }
}
val videoInfo = this.getVideoStat(bvid)
val viewCount = this.getViewCount(bvid, videoInfo.data.cid)
val shortUrl = this.generateShortUrl(bvid, videoInfo.data.aid)
val fans = Http.get<UserStat>(USER_STAT_URL, mapOf("vmid" to videoInfo.data.owner.mid)).data.follower
val authorFace = videoInfo.data.owner.face
val title = videoInfo.data.title
val author = videoInfo.data.owner.name
val coverPicUrl = videoInfo.data.pic
val view = videoInfo.data.stat.view.formatNumber()
val share = videoInfo.data.stat.share.formatNumber()
val like = videoInfo.data.stat.like.formatNumber()
val favorite = videoInfo.data.stat.favorite.formatNumber()
val coin = videoInfo.data.stat.coin.formatNumber()
val reply = videoInfo.data.stat.reply.formatNumber()
val image = this.createResponseImage(
title, author, authorFace,
coverPicUrl, reply, view, coin,
share, like, favorite, fans, viewCount
)
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addImage(image, true)
.addText(shortUrl)
.build()
listener.sendGroupMessage(message.groupId, msg)
}
}
@@ -0,0 +1,291 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/9/6
*/
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.entity.bili.*
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.misc.drawCustomImage
import cn.rtast.fancybot.util.misc.drawString
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.formatNumber
import cn.rtast.fancybot.util.str.fromJson
import cn.rtast.fancybot.util.str.setTruncate
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import okhttp3.OkHttpClient
import okhttp3.Request
import java.awt.Color
import java.awt.Font
import java.awt.Image
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.math.BigInteger
import java.net.URI
import javax.imageio.ImageIO
object BiliVideoParseCommand {
private val avRegex = Regex("av(\\d+)", RegexOption.IGNORE_CASE)
private val bvRegex = Regex("BV[\\dA-Za-z]{10}")
private val shortUrlRegex = Regex("https://b23\\.tv/\\w{6,8}")
private val backgroundColor = Color(43, 43, 43)
private val upBarColor = Color(102, 102, 102)
private val textColor = Color.WHITE
private val titleFont = Font("Serif", Font.ITALIC, 40)
private val numberFont = Font("Serif", Font.ITALIC, 25)
private val twoTwoLogo = ImageIO.read(Resources.loadFromResources("bili/22-coin.png"))
private val likeIcon = ImageIO.read(Resources.loadFromResources("bili/like.png"))
private val coinIcon = ImageIO.read(Resources.loadFromResources("bili/coin.png"))
private val viewIcon = ImageIO.read(Resources.loadFromResources("bili/view.png"))
private val shareIcon = ImageIO.read(Resources.loadFromResources("bili/share.png"))
private val favoriteIcon = ImageIO.read(Resources.loadFromResources("bili/favorite.png"))
private val replyIcon = ImageIO.read(Resources.loadFromResources("bili/reply.png"))
private const val VIDEO_STAT_URL = "https://api.bilibili.com/x/web-interface/view"
private const val USER_STAT_URL = "https://api.bilibili.com/x/relation/stat"
private const val SHORT_URL_API_URL = "https://api.bilibili.com/x/share/click"
private const val VIEW_COUNT_URL = "https://api.bilibili.com/x/player/online/total"
private const val CANVAS_WIDTH = 1000
private const val CANVAS_HEIGHT = 600
private const val BASE = 58
private const val DATA = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
private val XOR_CODE = BigInteger.valueOf(23442827791579L)
private val MAX_AID = BigInteger.ONE.shiftLeft(51)
private val tempOkHttpClient = OkHttpClient()
private fun generateShortUrl(bvid: String, oid: Long): String {
val result = Http.post<ShortUrl>(
SHORT_URL_API_URL, mapOf(
"buvid" to bvid,
"build" to 6114514,
"platform" to "unix",
"share_channel" to "COPY",
"share_mode" to 2,
"share_id" to "main.ugc-video-detail.0.0.pv",
"oid" to oid
)
)
return result.data.content.split(" ").last()
}
private fun getViewCount(bvid: String, cid: Long): String {
return Http.get<ViewCount>(
VIEW_COUNT_URL, mapOf(
"bvid" to bvid,
"cid" to cid
)
).data.total
}
private fun getVideoStat(bvid: String): VideoStat {
return Http.get<VideoStat>(VIDEO_STAT_URL, mapOf("bvid" to bvid))
}
private fun createResponseImage(
title: String,
author: String,
authorFace: String,
picUrl: String,
reply: String,
view: String,
coin: String,
share: String,
like: String,
favorite: String,
fans: Int,
viewCount: String,
): String {
val coverImage = ImageIO.read(URI(picUrl).toURL())
val faceImage = ImageIO.read(URI(authorFace).toURL())
val canvas = BufferedImage(CANVAS_WIDTH, CANVAS_HEIGHT, BufferedImage.TYPE_INT_RGB)
val g2d = canvas.createGraphics()
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.color = backgroundColor
g2d.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
// draw rect
g2d.color = upBarColor
g2d.fillRect(0, 0, CANVAS_WIDTH, 120)
g2d.color = textColor
// draw numbers
g2d.font = numberFont
g2d.drawString(like, 110, 200)
g2d.drawString(coin, 110, 290)
g2d.drawString(view, 110, 380)
g2d.drawString(favorite, 300, 380)
g2d.drawString(reply, 300, 290)
g2d.drawString(share, 300, 200)
// draw video title
val truncatedText = setTruncate(title, g2d)
g2d.font = titleFont
g2d.drawString(truncatedText, 20, 70)
// draw author name
g2d.drawString(author, 40, 580)
// draw icons
g2d.drawCustomImage(favoriteIcon, 230, 340, 50.0, 50.0, false)
g2d.drawCustomImage(replyIcon, 230, 250, 50.0, 50.0, false)
g2d.drawCustomImage(shareIcon, 230, 160, 50.0, 50.0, false)
g2d.drawCustomImage(viewIcon, 40, 340, 50.0, 50.0, false)
g2d.drawCustomImage(coinIcon, 40, 250, 50.0, 50.0, false)
g2d.drawCustomImage(likeIcon, 40, 160, 50.0, 50.0, false)
// draw 22 logo
g2d.drawCustomImage(twoTwoLogo, 800, 450, 90.0, 160.0, false)
// draw fans count
g2d.font = numberFont
g2d.drawString(fans.formatNumber() + "粉丝", 120, 520)
// draw viewing count
g2d.drawString("${viewCount}人正在观看", 40, 460)
// draw author face
g2d.drawCustomImage(faceImage, 40, 480, 60.0, 60.0, true)
// draw cover image
g2d.drawCustomImage(coverImage, 430, 140, 600.0, 300.0, true)
g2d.dispose()
return canvas.toByteArray().encodeToBase64()
}
private fun getShortUrlBVID(shortUrl: String): String {
val request = Request.Builder().url(shortUrl.split(" ").last()).build()
val redirectedUrl = tempOkHttpClient.newCall(request).execute()
redirectedUrl.use {
return it.request.url.toString().split("/")[4].split("?").first()
}
}
private fun String.extractAv(): Long = avRegex.find(this)!!.groupValues[1].toLong()
private fun String.extractBv(): String = bvRegex.find(this)!!.value
private fun swap(array: CharArray, i: Int, j: Int) {
val temp = array[i]
array[i] = array[j]
array[j] = temp
}
private fun Long.avToBv(): String {
val aid = BigInteger.valueOf(this)
val bytes = charArrayOf('B', 'V', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0')
var bvIndex = bytes.size - 1
var tmp = MAX_AID.or(aid).xor(XOR_CODE)
while (tmp > BigInteger.ZERO) {
bytes[bvIndex] = DATA[tmp.mod(BigInteger.valueOf(BASE.toLong())).toInt()]
tmp = tmp.divide(BigInteger.valueOf(BASE.toLong()))
bvIndex--
}
swap(bytes, 3, 9)
swap(bytes, 4, 7)
return String(bytes)
}
suspend fun parse(listener: OneBotListener, message: GroupMessage) {
try {
val bvid = if (bvRegex.containsMatchIn(message.rawMessage)) {
message.rawMessage.extractBv()
} else if (avRegex.containsMatchIn(message.rawMessage)) {
message.rawMessage.extractAv().avToBv()
} else if (shortUrlRegex.containsMatchIn(message.rawMessage)) {
val shortUrl = shortUrlRegex.find(message.rawMessage)!!.value
getShortUrlBVID(shortUrl)
} else if (bvRegex.containsMatchIn(message.rawMessage)) {
bvRegex.find(message.rawMessage)!!.value
} else if (message.message.find { it.type == ArrayMessageType.json } != null) {
val card = message.message.find { it.type == ArrayMessageType.json }!!
.data.data!!.toString().fromJson<CardShare>()
val shortUrl = if (card.meta.detail == null) {
if (!card.meta.news?.tag!!.contains("哔哩哔哩")) return
card.meta.news.jumpUrl
} else {
if (!card.meta.detail.title.contains("哔哩哔哩")) return
card.meta.detail.qqDocUrl
}
getShortUrlBVID(shortUrl)
} else {
return
}
val videoInfo = this.getVideoStat(bvid)
val viewCount = this.getViewCount(bvid, videoInfo.data.cid)
val shortUrl = this.generateShortUrl(bvid, videoInfo.data.aid)
val fans = Http.get<UserStat>(USER_STAT_URL, mapOf("vmid" to videoInfo.data.owner.mid)).data.follower
val authorFace = videoInfo.data.owner.face
val title = videoInfo.data.title
val author = videoInfo.data.owner.name
val coverPicUrl = videoInfo.data.pic
val view = videoInfo.data.stat.view.formatNumber()
val share = videoInfo.data.stat.share.formatNumber()
val like = videoInfo.data.stat.like.formatNumber()
val favorite = videoInfo.data.stat.favorite.formatNumber()
val coin = videoInfo.data.stat.coin.formatNumber()
val reply = videoInfo.data.stat.reply.formatNumber()
val image = this.createResponseImage(
title, author, authorFace,
coverPicUrl, reply, view, coin,
share, like, favorite, fans, viewCount
)
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addImage(image, true)
.addText(shortUrl)
.build()
listener.sendGroupMessage(message.groupId, msg)
} catch (_: Exception) {
}
}
}
object BiliUserParseCommand {
private val idRegex = Regex("https://space\\.bilibili\\.com/(\\d+)")
private const val USER_INFO_API_URL = "https://api.bilibili.com/x/web-interface/card"
private val font = Font("Serif", Font.BOLD, 24)
private val signFont = Font("Serif", Font.BOLD, 18)
private const val IMAGE_WIDTH = 500
private const val IMAGE_HEIGHT = 300
private fun getUserInfo(userId: String): BiliUserInfo {
val result = Http.get<UserInfo>(USER_INFO_API_URL, mapOf("mid" to userId)).data.card
return BiliUserInfo(result.name, result.levelInfo.currentLevel, result.face, result.fans, result.sign)
}
private fun getUserFace(url: String) = url.toURL().readBytes().toBufferedImage()
private fun createUserImage(userInfo: BiliUserInfo): String {
val image = BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB)
val g2d = image.createGraphics()
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.color = Color.WHITE
g2d.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT)
g2d.font = font
g2d.color = Color.BLACK
g2d.drawString("用户名: ${userInfo.name}", 150, 50)
g2d.drawString("粉丝数: ${userInfo.fans}", 150, 100)
g2d.drawString("等级: ${userInfo.level}", 150, 150)
g2d.font = signFont
g2d.drawString(setTruncate("签名: ${userInfo.sign}", g2d), 40, 180, 100)
val faceImage = getUserFace(userInfo.avatar)
val resizedFaceImage = faceImage.getScaledInstance(100, 100, Image.SCALE_SMOOTH)
g2d.drawImage(resizedFaceImage, 20, 20, null)
g2d.dispose()
return image.toByteArray().encodeToBase64()
}
suspend fun parse(listener: OneBotListener, message: GroupMessage) {
val match = idRegex.find(message.rawMessage)
if (match != null) {
val userId = match.groupValues[1]
val userInfo = this.getUserInfo(userId)
val userCardImage = this.createUserImage(userInfo)
val msg = MessageChain.Builder()
.addImage(userCardImage, true)
.build()
listener.sendGroupMessage(message.groupId, msg)
}
}
}
@@ -18,7 +18,6 @@ object CalculateCommand {
return try {
engine.eval(input)
} catch (_: ScriptException) {
"Invalid expression"
return null
}.toString()
}
@@ -0,0 +1,107 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/13
*/
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.commands.misc.ShortLinkCommand.Companion.makeShortLink
import cn.rtast.fancybot.entity.douyin.DouyinVideo
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.Logger
import cn.rtast.fancybot.util.misc.drawCustomImage
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.formatNumber
import cn.rtast.fancybot.util.str.proxy
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.ob.MessageChain
import okhttp3.OkHttpClient
import okhttp3.Request
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.geom.RoundRectangle2D
import java.awt.image.BufferedImage
object DouyinVideoParseCommand {
private val tempHttpClient = OkHttpClient.Builder().followRedirects(true).followSslRedirects(true).build()
private val shortLinkRegex = Regex("https://v\\.douyin\\.com/[a-zA-Z0-9]+/")
private val singleVideoRegex = Regex("https://www\\.douyin\\.com/video/([0-9]+)")
private val douyinApiUrl = "https://douyin.wtf/api/douyin/web/fetch_one_video".proxy
private val logger = Logger.getLogger<DouyinVideoParseCommand>()
private const val IMAGE_WIDTH = 1200
private const val IMAGE_HEIGHT = 800
private fun extractVideoId(origin: String) = singleVideoRegex.find(origin)?.groupValues?.get(1)!!
private fun createDouyinVideoCard(video: DouyinVideo): String {
val card = BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB)
val g2d = card.createGraphics()
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.color = Color(245, 245, 245)
g2d.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT)
g2d.color = Color(230, 230, 230)
g2d.fill(
RoundRectangle2D.Float(20f, 20f, (IMAGE_WIDTH - 40).toFloat(), (IMAGE_HEIGHT - 40).toFloat(), 50f, 50f)
)
g2d.color = Color.BLACK
g2d.font = Font("Serif", Font.BOLD, 48)
g2d.drawString(video.data.awemeDetail.author.nickname, 60, 100)
g2d.font = Font("Serif", Font.PLAIN, 32)
g2d.drawString(video.data.awemeDetail.desc, 60, 160)
val avatarImage = video.data.awemeDetail.author.avatarThumb.urlList
.first().toURL().readBytes().toBufferedImage()
g2d.drawImage(avatarImage, 60, 180, 160, 160, null)
val cover = video.data.awemeDetail.video.coverOriginalScale
g2d.drawCustomImage(
cover.urlList.first().toURL().readBytes().toBufferedImage(),
450, 170, cover.width.toDouble(), cover.height.toDouble()
)
g2d.color = Color.DARK_GRAY
g2d.font = Font("Serif", Font.PLAIN, 28)
g2d.drawString("粉丝: ${video.data.awemeDetail.author.followerCount.formatNumber()}", 60, 400)
g2d.drawString("点赞: ${video.data.awemeDetail.statistics.diggCount.formatNumber()}", 60, 440)
g2d.drawString("播放: ${video.data.awemeDetail.statistics.playCount.formatNumber()}", 60, 480)
g2d.drawString("评论: ${video.data.awemeDetail.statistics.commentCount.formatNumber()}", 60, 520)
g2d.drawString("收藏: ${video.data.awemeDetail.statistics.collectCount.formatNumber()}", 60, 560)
g2d.drawString("分享: ${video.data.awemeDetail.statistics.shareCount.formatNumber()}", 60, 600)
g2d.dispose()
return card.toByteArray().encodeToBase64()
}
suspend fun parse(message: GroupMessage) {
try {
val id = if (shortLinkRegex.find(message.rawMessage) != null) {
val shortUrl = shortLinkRegex.find(message.rawMessage)?.value ?: ""
val request = Request.Builder().url(shortUrl).build()
val finalUrl = tempHttpClient.newCall(request).execute().also { it.close() }
extractVideoId(finalUrl.request.url.toString())
} else if (singleVideoRegex.find(message.rawMessage) != null) extractVideoId(message.rawMessage) else ""
if (id.isNotBlank()) {
val response = Http.get<DouyinVideo>(
douyinApiUrl,
mapOf("aweme_id" to id)
)
val videoPlayUrlShortUrl = response.data.awemeDetail.video.bitRate
.first().playAddr.urlList.first().makeShortLink()
val shareUrlShortUrl = response.data.awemeDetail.shareUrl.makeShortLink()
val image = this.createDouyinVideoCard(response)
val msg = MessageChain.Builder()
.addImage(image, true)
.addText("视频地址: $shareUrlShortUrl")
.addNewLine()
.addText("下载地址: $videoPlayUrlShortUrl")
.build()
message.reply(msg)
}
} catch (e: Exception) {
logger.warn("抖音视频解析失败: ${e.message}")
}
}
}
@@ -10,16 +10,15 @@ package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.github.RepoInfo
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.Resources
import cn.rtast.fancybot.util.drawCustomImage
import cn.rtast.fancybot.util.drawString
import cn.rtast.fancybot.util.misc.Resources
import cn.rtast.fancybot.util.misc.drawCustomImage
import cn.rtast.fancybot.util.misc.drawString
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.formatNumberEnglish
import cn.rtast.fancybot.util.str.setTruncate
import cn.rtast.fancybot.util.toByteArray
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
@@ -42,6 +41,7 @@ object GitHubParseCommand {
private val descriptionCustomFont = Font("Serif", Font.PLAIN, 40).deriveFont(Font.BOLD or Font.ITALIC)
private val backgroundColor = Color.WHITE
private val textColor = Color(60, 60, 60)
private val githubRegex = Regex("""github\.com/([^/]+)/([^/]+)""")
private val languageColors = mapOf(
"Kotlin" to Color(169, 123, 255),
@@ -52,7 +52,7 @@ object GitHubParseCommand {
"Go" to Color(0, 173, 216),
"Ruby" to Color(112, 21, 22),
"PHP" to Color(79, 93, 149),
"TypeScript" to Color(43, 116, 137),
"TypeScript" to Color(0x3178c6),
"Swift" to Color(240, 81, 56),
"SCSS" to Color(198, 83, 140),
"Visual Basic 6.0" to Color(44, 99, 83),
@@ -63,17 +63,72 @@ object GitHubParseCommand {
"Shell" to Color(137, 224, 81),
"CMake" to Color(218, 52, 52),
"HTML" to Color(227, 76, 38),
"C#" to Color(23, 134, 0),
"F#" to Color(184, 69, 252),
"Scala" to Color(194, 45, 64),
"CSS" to Color(86, 61, 124),
"XML" to Color(0, 96, 172),
"Dart" to Color(0, 180, 171),
"Objective-C" to Color(67, 142, 255),
"OCaml" to Color(239, 122, 8),
"Jupyter Notebook" to Color(218, 91, 11),
"Haskell" to Color(239, 122, 8),
"GDScript" to Color(0x355570),
"Elm" to Color(0x60b5cc),
"Dockerfile" to Color(0x384d54),
"C" to Color(0x555555),
"Batchfile" to Color(0xC1, 0xF7, 0x65),
"AngelScript" to Color(0xC7d7dc),
"Vue" to Color(0x41b883),
"SQF" to Color(0x3f3f3f),
"R" to Color(0x198ce7),
"Assembly" to Color(0x6e4c13),
"M" to Color(0x9a6700),
"HCL" to Color(0x844fba),
"Scilab" to Color(0xca0f21),
"TeX" to Color(0x3d6117),
"Makefile" to Color(0x427819),
"Svelte" to Color(0xff3e00),
"Stylus" to Color(0xff6347),
"EJS" to Color(0xa91e50),
"PLpgSQL" to Color(0x336790),
"Markdown" to Color(0x083fa1),
"Lua" to Color(0x000080),
"JSON" to Color(0x292929),
"YAML" to Color(0xcb171e),
"HLSL" to Color(0xaace60),
"GLSL" to Color(0x5686a5),
"Jinja" to Color(0xa52a22),
"Zig" to Color(0xec915c),
"Perl" to Color(0x0298c3),
"Vim Script" to Color(0x199f4b),
"Tcl" to Color(0xe4cc98),
"Roff" to Color(0xecdebe),
"CoffeeScript" to Color(0x244776),
"Matlab" to Color(0xe16737),
"Pascal" to Color(0xE3F171),
"Fortran" to Color(0x4d41b1),
"Prolog" to Color(0x74283c),
"Erlang" to Color(0xB83998),
"D" to Color(0xba595e),
"ActionScript" to Color(0x882B0F),
"Mathematica" to Color(0xdd1100),
"Nim" to Color(0xffc200),
"COBOL" to Color(0x9a6700),
"Cuda" to Color(0x3a4e3a),
"SAS" to Color(0xb34936),
"Visual Basic .NET" to Color(0x945db7),
"Less" to Color(0x1d365d),
"Unknown" to Color(211, 211, 211)
)
private fun getLanguageColor(language: String): Color {
private fun getLanguageColor(language: String?): Color {
return languageColors[language] ?: languageColors["Unknown"]!!
}
private fun getRepoStat(username: String, repo: String): RepoInfo {
return Http.get<RepoInfo>(
"$REPO_INFO_API/$username/$repo",
headers = mapOf("Authorization" to "Bearer ${configManager.githubKey}")
"$REPO_INFO_API/$username/$repo", headers = mapOf("Authorization" to "Bearer ${configManager.githubKey}")
)
}
@@ -110,24 +165,26 @@ object GitHubParseCommand {
g2d.drawString(repoStat.description ?: "暂无描述~", 80, 400, 800)
val avatarImage = ImageIO.read(URI(repoStat.owner.avatarUrl).toURL())
g2d.drawCustomImage(avatarImage, 1200, 160, 300.0, 300.0, true)
g2d.color = this.getLanguageColor(repoStat.language)
g2d.drawString(repoStat.language ?: "[无语言]", 30, CANVAS_HEIGHT - 50)
g2d.dispose()
return canvas.toByteArray().encodeToBase64()
}
suspend fun parse(listener: OneBotListener, message: GroupMessage) {
val path = message.rawMessage
.replace(".git", "")
.replace("https://github.com/", "")
.replace("git@github.com:", "")
.split("/")
val user = path.first()
val repo = path.last()
suspend fun parse(message: GroupMessage) {
val matchedResult = githubRegex.find(message.rawMessage)
if (message.rawMessage.contains("github.com") && matchedResult != null) {
val (user, repo) = matchedResult.destructured
val repoStat = this.getRepoStat(user, repo)
val image = this.createImage(repoStat)
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addImage(image, true)
.build()
listener.sendGroupMessage(message.groupId, msg)
val msg = MessageChain.Builder().addImage(image, true).build()
message.reply(msg)
}
}
fun creatRepoImage(user: String, repo: String): String {
val repoStat = this.getRepoStat(user, repo)
val image = this.createImage(repoStat)
return image
}
}
@@ -7,24 +7,52 @@
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.commands.misc.ShortLinkCommand.Companion.makeShortLink
import cn.rtast.fancybot.configManager
import cn.rtast.rob.entity.GetMessage
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.asNode
object ImageURLCommand {
fun getImageUrl(message: GetMessage.Data): List<String> {
try {
val images = mutableListOf<String>()
message.message.forEach {
if (it.type == ArrayMessageType.image) images.add(it.data.file!!)
if (it.type == ArrayMessageType.mface) images.add(it.data.url!!)
}
return images
} catch (_: NullPointerException) {
return emptyList()
}
}
suspend fun callback(groupMessage: GroupMessage, message: GetMessage.Data) {
val msg = MessageChain.Builder()
.addReply(message.messageId)
.addText("哈哈哈哈图片来咯~")
.addNewLine()
val image = message.message.find { it.type == ArrayMessageType.image }
if (image == null) {
val gif = message.message.find { it.type == ArrayMessageType.mface }!!
msg.addText(gif.data.url!!)
val images = getImageUrl(message)
if (images.isEmpty()) {
groupMessage.reply("这个消息里没有图片呢!")
} else {
msg.addText(image.data.file!!)
if (images.size == 1) {
val msg = MessageChain.Builder()
.addText(images.first().makeShortLink())
.addNewLine(2)
.addText(images.first())
.build()
groupMessage.reply(msg)
} else {
val messages = mutableListOf<MessageChain>()
images.forEach {
val msg = MessageChain.Builder()
.addText(it)
.addNewLine(2)
.build()
messages.add(msg)
}
groupMessage.reply(messages.asNode(configManager.selfId))
}
}
groupMessage.reply(msg.build())
}
}
@@ -8,8 +8,9 @@
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.annotations.CommandDescription
import cn.rtast.fancybot.util.makeGif
import cn.rtast.fancybot.util.misc.makeGif
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.rob.entity.GetMessage
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.enums.ArrayMessageType
import cn.rtast.rob.util.BaseCommand
@@ -34,6 +35,20 @@ class ReverseGIFCommand : BaseCommand() {
return gifBytes.encodeToBase64()
}
suspend fun reverse(message: GroupMessage, getMsg: GetMessage.Data) {
val imageObject = getMsg.message.find { it.type == ArrayMessageType.image }
if (imageObject == null) {
message.reply("这个消息中没有图片呢")
} else {
val url = imageObject.data.file!!
val image = this.reverseGif(url)
val msg = MessageChain.Builder()
.addImage(image, true)
.build()
message.reply(msg)
}
}
suspend fun callback(message: GroupMessage) {
if (message.sender.userId !in waitingList) return
waitingList.removeIf { it == message.sender.userId }
@@ -0,0 +1,85 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/15
*/
package cn.rtast.fancybot.commands.parse
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.entity.yt.YoutubeVideoResponse
import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.misc.toBufferedImage
import cn.rtast.fancybot.util.misc.toByteArray
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.formatNumber
import cn.rtast.fancybot.util.str.proxy
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.ob.MessageChain
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.image.BufferedImage
object YoutubeVideoParseCommand {
private const val YOUTUBE_API_URL = "https://proxy.rtast.cn/https/www.googleapis.com/youtube/v3/videos"
private const val IMAGE_WIDTH = 800
private const val IMAGE_HEIGHT = 450
private val dataApiKey = configManager.youtubeDataApiKey
private val youtubeVideoIdRegex =
Regex("(?:https?://)?(?:www\\.)?(?:youtube\\.com/(?:watch\\?v=|embed/|v/|.+\\?v=)|youtu\\.be/)([\\w\\-]{11})")
private fun createVideoCard(videoInfo: YoutubeVideoResponse): String {
val image = BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB)
val g2d = image.createGraphics()
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
g2d.color = Color(48, 48, 48)
g2d.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT)
g2d.color = Color(255, 255, 255)
g2d.font = Font("Serif", Font.BOLD, 24)
val title = videoInfo.items.first().snippet.title
g2d.drawString(title, 20, 40)
g2d.font = Font("Serif", Font.PLAIN, 18)
val channelTitle = videoInfo.items.first().snippet.channelTitle
g2d.drawString(channelTitle, 20, 70)
val thumbnailImage = videoInfo.items.first().snippet.thumbnails.standard.url
.proxy.toURL().readBytes().toBufferedImage()
val scaledWidth = 300
val scaledHeight = (thumbnailImage.height * (scaledWidth / thumbnailImage.width.toDouble())).toInt()
g2d.drawImage(thumbnailImage, 20, 100, scaledWidth, scaledHeight, null)
g2d.color = Color(200, 200, 200)
g2d.font = Font("Serif", Font.PLAIN, 16)
g2d.drawString("观看次数: ${videoInfo.items.first().statistics.viewCount.toInt().formatNumber()}", 340, 120)
g2d.drawString("点赞次数: ${videoInfo.items.first().statistics.likeCount.toInt().formatNumber()}", 340, 150)
g2d.drawString("收藏次数: ${videoInfo.items.first().statistics.favoriteCount.toInt().formatNumber()}", 340, 180)
g2d.drawString("评论数: ${videoInfo.items.first().statistics.commentCount.toInt().formatNumber()}", 340, 210)
g2d.dispose()
return image.toByteArray().encodeToBase64()
}
private fun getVideoInfo(videoId: String): YoutubeVideoResponse {
val response = Http.get<YoutubeVideoResponse>(
YOUTUBE_API_URL,
mapOf(
"id" to videoId,
"key" to dataApiKey,
"part" to "snippet,statistics"
)
)
return response
}
suspend fun parse(message: GroupMessage) {
val matchResult = youtubeVideoIdRegex.find(message.rawMessage)
if (matchResult != null) {
val videoId = matchResult.groupValues[1]
val videoInfo = this.getVideoInfo(videoId)
val image = createVideoCard(videoInfo)
val msg = MessageChain.Builder().addImage(image, true).build()
message.reply(msg)
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/13
*/
package cn.rtast.fancybot.commands.reply
import cn.rtast.fancybot.commands.misc.ShortLinkCommand.Companion.makeShortLink
import cn.rtast.fancybot.commands.parse.ImageURLCommand
import cn.rtast.fancybot.configManager
import cn.rtast.fancybot.util.file.getFileType
import cn.rtast.fancybot.util.misc.ImageBed
import cn.rtast.fancybot.util.misc.toURL
import cn.rtast.rob.entity.GetMessage
import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.asNode
import okio.IOException
object ImageBedCommand {
suspend fun execute(getMsg: GetMessage.Data, message: GroupMessage) {
val imagesUrl = ImageURLCommand.getImageUrl(getMsg)
if (imagesUrl.isEmpty()) {
message.reply("这个消息里没有图片呢!")
} else {
try {
if (imagesUrl.size == 1) {
val imageByteArray = imagesUrl.first().toURL().readBytes()
val imageFileType = imageByteArray.getFileType()
val imageBedUrl = ImageBed.upload(imageByteArray, imageFileType)
val msg = MessageChain.Builder()
.addText(imageBedUrl.makeShortLink())
.addNewLine(2)
.addText(imageBedUrl)
.build()
message.reply(msg)
} else {
val messages = mutableListOf<MessageChain>()
imagesUrl.forEach {
val imageByteArray = it.toURL().readBytes()
val imageFileType = imageByteArray.getFileType()
val imageBedUrl = ImageBed.upload(imageByteArray, imageFileType)
val msg = MessageChain.Builder()
.addText(imageBedUrl.makeShortLink())
.addNewLine(2)
.addText(imageBedUrl)
.build()
messages.add(msg)
}
message.reply(messages.asNode(configManager.selfId))
}
} catch (_: IOException) {
message.reply("上传失败: 图片已过期")
} catch (e: Exception) {
e.printStackTrace()
message.reply("上传失败: ${e.message}")
}
}
}
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/2
*/
package cn.rtast.fancybot.db
import org.jetbrains.exposed.sql.Table
object ActionTable : Table("action") {
val id = integer("id").autoIncrement()
val action = varchar("action", 255)
val timestamp = long("timestamp")
val triggerId = long("trigger_id")
val result = text("result")
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,24 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.db
import org.jetbrains.exposed.sql.Table
object BlackListTable : Table("blacklist") {
val id = integer("id").autoIncrement()
val groupId = long("group_id")
val operator = long("operator")
val timestamp = long("timestamp")
override val primaryKey = PrimaryKey(id)
}
data class BlackListEntity(
val groupId: Long,
val operator: Long,
val timestamp: Long,
)
@@ -5,7 +5,7 @@
*/
package cn.rtast.fancybot.entity.db
package cn.rtast.fancybot.db
import org.jetbrains.exposed.sql.Table
@@ -0,0 +1,63 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.db
import cn.rtast.fancybot.db.MinecraftAccessTokenTable.timestamp
import cn.rtast.fancybot.db.MinecraftAccessTokenTable.token
import cn.rtast.fancybot.db.MinecraftAccessTokenTable.userId
import cn.rtast.fancybot.util.suspendedTransaction
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import java.time.Instant
object MinecraftAccessTokenTable : Table("mc_access_token") {
val id = integer("id").autoIncrement()
val token = text("token")
val userId = long("user_id")
val timestamp = long("timestamp")
override val primaryKey = PrimaryKey(id)
}
data class MCAccessTokenEntity(
val token: String,
val userId: Long,
val timestamp: Long
)
suspend fun insertNewAccessToken(accessToken: String, id: Long) {
suspendedTransaction {
MinecraftAccessTokenTable.insert {
it[token] = accessToken
it[userId] = id
it[timestamp] = Instant.now().epochSecond
}
}
}
suspend fun getAccessTokenById(id: Long): MCAccessTokenEntity? {
return suspendedTransaction {
MinecraftAccessTokenTable.selectAll()
.where { userId eq id }
.map {
MCAccessTokenEntity(
it[token],
it[userId],
it[timestamp]
)
}.singleOrNull()
}
}
suspend fun deleteAccessTokenById(id: Long) {
suspendedTransaction {
MinecraftAccessTokenTable.deleteWhere { userId eq id }
}
}
@@ -5,7 +5,7 @@
*/
package cn.rtast.fancybot.entity.db
package cn.rtast.fancybot.db
import org.jetbrains.exposed.sql.Table
@@ -5,7 +5,7 @@
*/
package cn.rtast.fancybot.entity.db
package cn.rtast.fancybot.db
import org.jetbrains.exposed.sql.Table
@@ -0,0 +1,29 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/13
*/
package cn.rtast.fancybot.db
import org.jetbrains.exposed.sql.Table
object RCONTable : Table("rcon") {
val id = integer("id").autoIncrement()
val host = varchar("host", 50)
val port = integer("port")
val userId = long("user_id")
val password = varchar("password", 128)
val name = varchar("name", 50)
override val primaryKey = PrimaryKey(id)
}
data class RCONEntity(
val host: String,
val port: Int,
val userId: Long,
val name: String,
val password: String
)
@@ -7,10 +7,13 @@
package cn.rtast.fancybot.entity
import cn.rtast.fancybot.entity.enums.ImageType
import cn.rtast.fancybot.entity.enums.WSType
import cn.rtast.fancybot.enums.DatabaseType
import cn.rtast.fancybot.enums.ImageBedType
import cn.rtast.fancybot.enums.ImageType
import cn.rtast.fancybot.enums.WSType
data class Config(
val database: Database,
val ncmAPI: String,
val wsType: WSType,
val wsAddress: String,
@@ -33,6 +36,28 @@ data class Config(
val llamaUrl: String,
val llamaModel: String,
val qqMusicApiUrl: String,
val startUpNoticeUser: Long,
val selfId: Long
val noticeUser: Long,
val selfId: Long,
val tianXingApiKey: String,
val githubUser: String,
val githubImageRepo: String,
val imageBedType: ImageBedType,
val cloudflareAccountId: String,
val cloudflareR2AccessKeyId: String,
val cloudflareR2SecretKey: String,
val cloudflareR2BucketName: String,
val cloudflareR2PublicUrl: String,
val apiSpaceKey: String,
val apiRtastKey: String,
val azureAppClientId: String,
val youtubeDataApiKey: String,
) {
data class Database(
val type: DatabaseType,
val host: String,
val port: Int,
val user: String,
val password: String,
val database: String,
)
}
@@ -0,0 +1,15 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/23
*/
package cn.rtast.fancybot.entity
data class IPGeo(
val city: String,
val region: String,
val country: String,
val org: String,
)
@@ -1,12 +0,0 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/9/22
*/
package cn.rtast.fancybot.entity
data class ShortLink(
val id: String
)
@@ -0,0 +1,19 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/10
*/
package cn.rtast.fancybot.entity.apispace
data class IdiomExplainResponse(
val data: List<Data>
) {
data class Data(
val name: String,
val sound: String,
val explanation: String,
val provenance: String,
)
}
@@ -0,0 +1,16 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/12
*/
package cn.rtast.fancybot.entity.bili
data class BiliUserInfo(
val name: String,
val level: Int,
val avatar: String,
val fans: Int,
val sign: String,
)
@@ -0,0 +1,32 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/12
*/
package cn.rtast.fancybot.entity.bili
import com.google.gson.annotations.SerializedName
data class UserInfo(
val data: Data
) {
data class Data(
val card: Card,
)
data class Card(
val face: String,
val name: String,
val fans: Int,
val sign: String,
@SerializedName("level_info")
val levelInfo: LevelInfo
)
data class LevelInfo(
@SerializedName("current_level")
val currentLevel: Int,
)
}
@@ -0,0 +1,84 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/13
*/
package cn.rtast.fancybot.entity.douyin
import com.google.gson.annotations.SerializedName
data class DouyinVideo(
val data: Data
) {
data class Data(
@SerializedName("aweme_detail")
val awemeDetail: AwemeDetail
)
data class AwemeDetail(
val author: Author,
val desc: String,
val statistics: Statistics,
val video: Video,
@SerializedName("share_url")
val shareUrl: String,
)
data class Author(
val nickname: String,
@SerializedName("follower_count")
val followerCount: Int,
val signature: String,
@SerializedName("avatar_thumb")
val avatarThumb: AvatarThumb,
)
data class AvatarThumb(
@SerializedName("url_list")
val urlList: List<String>,
)
data class Statistics(
// 收藏数
@SerializedName("collect_count")
val collectCount: Int,
// 评论数
@SerializedName("comment_count")
val commentCount: Int,
// 点赞数
@SerializedName("digg_count")
val diggCount: Int,
// 播放数
@SerializedName("play_count")
val playCount: Int,
// 分享数
@SerializedName("share_count")
val shareCount: Int,
)
data class Video(
@SerializedName("cover_original_scale")
val coverOriginalScale: CoverOriginalScale,
@SerializedName("bit_rate")
val bitRate: List<BitRate>
)
data class BitRate(
@SerializedName("play_addr")
val playAddr: PlayAddr
)
data class PlayAddr(
@SerializedName("url_list")
val urlList: List<String>,
)
data class CoverOriginalScale(
@SerializedName("url_list")
val urlList: List<String>,
val height: Int,
val width: Int
)
}
@@ -0,0 +1,23 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/3
*/
package cn.rtast.fancybot.entity.github
data class LatestCommit(
val sha: String,
val commit: Commit
) {
data class Commit(
val committer: Committer
)
data class Committer(
val email: String,
val name: String,
val date: String,
)
}
@@ -16,7 +16,7 @@ data class RepoInfo(
val description: String?,
@SerializedName("forks_count")
val forksCount: Int,
val language: String,
val language: String?,
@SerializedName("stargazers_count")
val starsCount: Int,
@SerializedName("open_issues_count")
@@ -0,0 +1,13 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.entity.github
data class UploadContentPayload(
val message: String,
val content: String,
)
@@ -0,0 +1,16 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.entity.github
data class UploadContentResponse(
val content: Content
) {
data class Content(
val name: String,
)
}
@@ -7,7 +7,7 @@
package cn.rtast.fancybot.entity.gpt
import cn.rtast.fancybot.entity.enums.GPTUserRole
import cn.rtast.fancybot.enums.GPTUserRole
data class ChatCompletionsPayload(
val model: String,
@@ -7,7 +7,7 @@
package cn.rtast.fancybot.entity.gpt
import cn.rtast.fancybot.entity.enums.GPTUserRole
import cn.rtast.fancybot.enums.GPTUserRole
data class ChatCompletionsResponse(
val choices: List<Choice>
@@ -0,0 +1,26 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/9
*/
package cn.rtast.fancybot.entity.mc
import cn.rtast.fancybot.enums.mc.MCVersionType
data class MCVersion(
val latest: Latest,
val versions: List<Version>
) {
data class Latest(
val release: String,
val snapshot: String
)
data class Version(
val id: String,
val type: MCVersionType,
val releaseTime: String
)
}
@@ -0,0 +1,31 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/10
*/
package cn.rtast.fancybot.entity.mc
import com.google.gson.annotations.SerializedName
data class MCWikiPageResponse(
@SerializedName("redirect_target")
val redirectTarget: String?,
val httpCode: Int?,
val id: Int,
val title: String,
val latest: Latest,
val source: String,
val license: License
) {
data class Latest(
val id: Int,
val timestamp: String,
)
data class License(
val url: String,
val title: String,
)
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/9
*/
package cn.rtast.fancybot.entity.mc
data class MCWikiSearchResponse(
val pages: List<Page>
) {
data class Page(
val id: Int,
val title: String,
val thumbnail: Thumbnail?
)
data class Thumbnail(
val url: String
)
}
@@ -0,0 +1,31 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/2
*/
package cn.rtast.fancybot.entity.mc
import com.google.gson.annotations.SerializedName
data class Skin(
val properties: List<Property>,
) {
data class Property(
val value: String,
)
}
data class DecodedSkin(
val textures: Texture,
) {
data class Texture(
@SerializedName("SKIN")
val skin: SKIN,
)
data class SKIN(
val url: String,
)
}
@@ -0,0 +1,10 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/2
*/
package cn.rtast.fancybot.entity.mc
data class Username(val id: String)
@@ -0,0 +1,19 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.entity.mc.oauth
import com.google.gson.annotations.SerializedName
data class DeviceCodeResponse(
@SerializedName("user_code")
val userCode: String,
@SerializedName("device_code")
val deviceCode: String,
@SerializedName("verification_uri")
val verificationUri: String
)
@@ -0,0 +1,28 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.entity.mc.oauth
import com.google.gson.annotations.SerializedName
data class LoginXboxLivePayload(
@SerializedName("Properties")
val properties: Properties,
@SerializedName("RelyingParty")
val relyingParty: String = "http://auth.xboxlive.com",
@SerializedName("TokenType")
val tokenType: String = "JWT",
) {
data class Properties(
@SerializedName("RpsTicket")
val rpsTicket: String,
@SerializedName("AuthMethod")
val authMethod: String = "RPS",
@SerializedName("SiteName")
val siteName: String = "user.auth.xboxlive.com",
)
}
@@ -0,0 +1,25 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.entity.mc.oauth
import com.google.gson.annotations.SerializedName
data class LoginXboxLiveResponse(
@SerializedName("Token")
val token: String,
@SerializedName("DisplayClaims")
val displayClaims: DisplayClaims,
) {
data class DisplayClaims(
val xui: List<XUI>
)
data class XUI(
val uhs: String
)
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.entity.mc.oauth
import com.google.gson.annotations.SerializedName
data class ObtainXSTSPayload(
@SerializedName("Properties")
val properties: Properties,
@SerializedName("RelyingParty")
val relyingParty: String = "rp://api.minecraftservices.com/",
@SerializedName("TokenType")
val tokenType: String = "JWT",
) {
data class Properties(
@SerializedName("UserTokens")
val userTokens: List<String>,
@SerializedName("SandboxId")
val sandboxId: String = "RETAIL",
)
}
@@ -0,0 +1,18 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.entity.mc.oauth
import cn.rtast.fancybot.entity.mc.oauth.LoginXboxLiveResponse.DisplayClaims
import com.google.gson.annotations.SerializedName
data class ObtainXSTSResponse(
@SerializedName("Token")
val token: String,
@SerializedName("DisplayClaims")
val displayClaims: DisplayClaims,
)
@@ -0,0 +1,15 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/11
*/
package cn.rtast.fancybot.entity.mc.oauth
import com.google.gson.annotations.SerializedName
data class RedeemCodeResponse(
@SerializedName("access_token")
val accessToken: String,
)
@@ -0,0 +1,20 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/3
*/
package cn.rtast.fancybot.entity.music
data class TopSong(
val data: List<Data>
) {
data class Data(
val privilege: Privilege
)
data class Privilege(
val id: Long
)
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/14
*/
package cn.rtast.fancybot.entity.pastebin
data class PastebinPayload(val content: String)
data class PastebinResponse(val id: String)
@@ -1,22 +1,17 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/9/29
* Date: 2024/10/6
*/
package cn.rtast.fancybot.entity.setu
data class SetuV2(
data class SetuV3(
val pid: Long,
val uid: Long,
val title: String,
val author: String,
val urls: URLS,
val url: String,
val r18: Boolean,
val tags: List<String>
) {
data class URLS(
val regular: String,
)
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.entity.shortlink
data class ShortLinkPayload(
val target: String
)
@@ -0,0 +1,12 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.entity.shortlink
data class ShortLinkResponse(
val id: String
)
@@ -0,0 +1,12 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.entity.thecat
data class Cat(
val url: String,
)
@@ -0,0 +1,24 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/8
*/
package cn.rtast.fancybot.entity.tianxing
import com.google.gson.annotations.SerializedName
data class HistoryOfToday(
val result: Result
) {
data class Result(
val list: List<Element>
)
data class Element(
val title: String,
@SerializedName("lsdate")
val lsDate: String
)
}
@@ -0,0 +1,15 @@
/*
* Copyright © 2024 RTAkland
* Author: RTAkland
* Date: 2024/10/7
*/
package cn.rtast.fancybot.entity.tts
import com.google.gson.annotations.SerializedName
data class TTSResponse(
@SerializedName("URL")
val url: String
)
Loaded 100 of 261 files, more files were not shown because too many files have changed in this diff. Show more