├── .github └── workflows │ └── build-and-upload.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── config-example.toml ├── config.example.json ├── destinybot-api ├── build.gradle └── src │ └── main │ └── kotlin │ ├── module-info.java │ └── net │ └── origind │ └── destinybot │ └── api │ ├── cache │ ├── Cache.kt │ ├── CacheManager.kt │ └── ETagJsonCache.kt │ ├── command │ ├── AbstractCommand.kt │ ├── AbstractCustomCommand.kt │ ├── ArgumentContainer.kt │ ├── ArgumentContext.kt │ ├── ArgumentParseException.kt │ ├── ArgumentType.kt │ ├── ArgumentTypes.kt │ ├── Command.kt │ ├── CommandContext.kt │ ├── CommandException.kt │ ├── CommandExecutor.kt │ ├── CommandManager.kt │ ├── CommandParser.kt │ ├── CommandSpec.kt │ ├── ConsoleCommandExecutor.kt │ ├── CustomCommand.kt │ └── UserCommandExecutor.kt │ ├── package-info.java │ ├── plugin │ ├── Plugin.kt │ └── PluginContainer.java │ ├── timer │ ├── TimedTask.kt │ └── TimerManager.kt │ └── util │ ├── DurationHelper.kt │ └── ExceptionHelper.kt ├── destinybot-core ├── build.gradle └── src │ └── main │ ├── kotlin │ ├── module-info.java │ └── net │ │ └── origind │ │ └── destinybot │ │ └── core │ │ ├── BotConfig.kt │ │ ├── DestinyBot.kt │ │ ├── TestBot.kt │ │ ├── command │ │ ├── AdminCommand.kt │ │ ├── AnnouncementCommand.kt │ │ ├── CommandManager.kt │ │ ├── ConfigCommand.kt │ │ ├── DeopCommand.kt │ │ ├── DistributionCommand.kt │ │ ├── GroupListCommand.kt │ │ ├── HelpCommand.kt │ │ ├── KickCommand.kt │ │ ├── MemberJoinRequestCommand.kt │ │ ├── MiraiUserCommandExecutor.kt │ │ ├── OpCommand.kt │ │ ├── OpsCommand.kt │ │ ├── RankingCommand.kt │ │ ├── ReloadCommand.kt │ │ ├── StatusCommand.kt │ │ └── SudoCommand.kt │ │ ├── task │ │ └── CheckStreamerTask.kt │ │ └── util │ │ ├── AnnouncementAdapter.kt │ │ ├── ConfigExtension.kt │ │ ├── ContactAdapter.kt │ │ ├── GZIPHelper.kt │ │ └── MemberData.kt │ └── resources │ ├── lang │ ├── lang.properties │ ├── lang_en_US.properties │ └── lang_zh_CN.properties │ ├── logback.xml │ └── messages.json ├── destinybot-features ├── build.gradle ├── chessboard.png └── src │ ├── main │ ├── kotlin │ │ ├── module-info.java │ │ └── net │ │ │ └── origind │ │ │ └── destinybot │ │ │ └── features │ │ │ ├── DataStore.kt │ │ │ ├── Database.kt │ │ │ ├── FeaturesPlugin.kt │ │ │ ├── NetworkHelper.kt │ │ │ ├── apex │ │ │ ├── ApexAPI.kt │ │ │ ├── MapRotationCommand.kt │ │ │ ├── ProfileCommand.kt │ │ │ └── response │ │ │ │ ├── ApexMapRotation.kt │ │ │ │ └── ApexPlayer.kt │ │ │ ├── bilibili │ │ │ ├── BilibiliAPI.kt │ │ │ ├── BilibiliConfig.kt │ │ │ ├── BilibiliResponses.kt │ │ │ ├── ManageStreamerCommand.kt │ │ │ ├── StreamerCommand.kt │ │ │ ├── VTuberCommand.kt │ │ │ ├── VdbAPI.kt │ │ │ └── vdb │ │ │ │ └── VdbResponses.kt │ │ │ ├── destiny │ │ │ ├── ActivityCommand.kt │ │ │ ├── BindAccountCommand.kt │ │ │ ├── BungieAPI.kt │ │ │ ├── DestinyExceptions.kt │ │ │ ├── DestinyManifestDatabase.kt │ │ │ ├── LightggAPI.kt │ │ │ ├── LoreCommand.kt │ │ │ ├── MyProfileCommand.kt │ │ │ ├── PerkCommand.kt │ │ │ ├── PlayerProfileCommand.kt │ │ │ ├── QueryLinkedCredentialCommand.kt │ │ │ ├── SearchChooseResultCommand.kt │ │ │ ├── SearchCommand.kt │ │ │ ├── TrackerAPI.kt │ │ │ ├── TrackerCommand.kt │ │ │ ├── WeeklyReportCommand.kt │ │ │ ├── data │ │ │ │ ├── Lore.kt │ │ │ │ └── UserData.kt │ │ │ ├── image │ │ │ │ ├── DestinyImageCache.kt │ │ │ │ ├── DestinyImageDrawer.kt │ │ │ │ └── ImageHelper.kt │ │ │ └── response │ │ │ │ ├── BungieMultiResponse.kt │ │ │ │ ├── BungieResponses.kt │ │ │ │ ├── BungieSingleResponse.kt │ │ │ │ ├── DestinyActivityDefinition.kt │ │ │ │ ├── DestinyMessageResponse.kt │ │ │ │ ├── lightgg │ │ │ │ ├── Perks.kt │ │ │ │ └── Wishlist.kt │ │ │ │ └── tracker │ │ │ │ ├── TrackerResponses.kt │ │ │ │ └── TrackerWeapon.kt │ │ │ ├── github │ │ │ ├── GitHubCache.kt │ │ │ ├── GitHubCommand.kt │ │ │ ├── GitHubCommit.kt │ │ │ └── GitHubCommitCommand.kt │ │ │ ├── injdk │ │ │ ├── InjdkCommand.kt │ │ │ └── InjdkDistribution.kt │ │ │ ├── instatus │ │ │ ├── InstatusAPI.kt │ │ │ ├── InstatusCommand.kt │ │ │ └── response │ │ │ │ ├── Component.kt │ │ │ │ └── Page.kt │ │ │ ├── minecraft │ │ │ ├── MinecraftServerAddressArgument.kt │ │ │ ├── MinecraftSpec.kt │ │ │ ├── MinecraftVersionCommand.kt │ │ │ ├── MinecraftVersionManifest.kt │ │ │ └── PingCommand.kt │ │ │ ├── romajitable │ │ │ ├── RomajiConverter.kt │ │ │ └── RomajiTable.kt │ │ │ ├── timer │ │ │ └── TimerCommand.kt │ │ │ └── yahtzee │ │ │ ├── Dice.kt │ │ │ ├── GameStage.kt │ │ │ ├── YahtzeeGame.kt │ │ │ ├── YahtzeeGameManager.kt │ │ │ └── YahtzeePlayer.kt │ └── resources │ │ ├── Katakana.json │ │ ├── META-INF │ │ └── services │ │ │ └── net.origind.destinybot.api.plugin.Plugin │ │ └── Romaji.json │ └── test │ └── kotlin │ ├── DestinyBotTest.kt │ └── TimerTest.kt ├── destinybot-manifest ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── README.md └── update_manifest.js └── settings.gradle /.github/workflows/build-and-upload.yml: -------------------------------------------------------------------------------- 1 | name: 'build and upload artifact' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: "ubuntu-20.04" 9 | steps: 10 | - name: "Checkout Repository" 11 | uses: "actions/checkout@v2.3.4" 12 | - name: "Setup JDK 16" 13 | uses: "actions/setup-java@v2.1.0" 14 | with: 15 | distribution: "adopt" 16 | java-version: "16" 17 | - name: "Give the permission!" 18 | run: "chmod +x gradlew" 19 | - name: "Clean Build" 20 | run: "./gradlew clean build -x test" 21 | - name: "Upload artifact" 22 | uses: "actions/upload-artifact@v2" 23 | with: 24 | name: "build" 25 | path: "./build/libs/*.jar" 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .gradle/ 26 | .idea/ 27 | build/ 28 | mirai/ 29 | run/ 30 | data/ 31 | target/ 32 | web_cache/ 33 | destinybot-manifest/*.json 34 | destiny2_images/ 35 | 36 | ..bfg-report/ 37 | config.toml 38 | config.json 39 | device.json 40 | version_manifest.json 41 | /cache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Destiny Bot - Mirai 版 2 | 3 | **本项目已废弃,新开发转向 [rust-destinybot](https://github.com/LasmGratel/rust-destinybot),面向 OneBot 11 API 开发。** 4 | 5 | LG 自用机器人。 6 | 7 | ## 结构 8 | 9 | 项目采用 Jigsaw 并使用 ServiceLoader 加载模块和插件。 10 | 11 | `net.origind.destinybot.api` 模块使用最少的依赖,负责日志,插件抽象,命令解析等基础代码。 12 | 13 | `net.origind.destinybot.features` 模块实现机器人的绝大部分功能,这些功能不需要依赖 QQ 或 Mirai 本身。 14 | 15 | `net.origind.destinybot.core` 模块实现与 Mirai 的交互,账号登陆,守护进程,也包括机器人的管理命令与帮助命令。 16 | 17 | ## 功能 18 | 19 | - [X] 命运2相关功能 20 | - [ ] Perk 查询 21 | - [X] 用户信息查询 22 | - [X] 传说故事查询 23 | - [X] 用户信息搜索 24 | - [X] Minecraft 相关功能 25 | - [X] 服务器 Ping 26 | - [X] https://howoldisminecraft1710.today/ (发送/1710) 27 | - [X] 哔哩哔哩相关功能 28 | - [X] 下饭主播 29 | - [ ] 查成分 30 | - [X] Apex Legends 相关功能 31 | - [X] 开盒 32 | - [X] 地图轮换 33 | - [X] GitHub 相关功能 34 | - [X] 查询最近 Commit 35 | - [X] Injdk 功能 36 | - [ ] Instatus 功能 37 | - [ ] 快速增加警告信息 38 | - [X] 管理功能 39 | - [X] 更改配置 40 | - [X] reload 41 | 42 | ## 使用 43 | 44 | `.\gradlew distZip` 打包所有需要的文件。解压后复制 `config-example.toml` 为 `config.toml` 并进行必要的配置修改,之后执行 `bin/destinybot` 即可。 45 | 46 | ## 协议 47 | 48 | [GPLv3](LICENSE) 49 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | id 'org.jetbrains.kotlin.jvm' version '1.7.21' 5 | id "org.sonarqube" version "3.3" 6 | } 7 | 8 | group 'net.origind.destinybot' 9 | version '1.0' 10 | 11 | sourceCompatibility = targetCompatibility = "17" 12 | 13 | repositories { 14 | mavenCentral() 15 | maven { url 'https://jitpack.io' } 16 | 17 | maven { url 'https://repo.opencollab.dev/maven-releases' } 18 | } 19 | 20 | application { 21 | mainClass = 'net.origind.destinybot.core.DestinyBot' 22 | } 23 | 24 | dependencies { 25 | implementation project(":destinybot-core") 26 | } 27 | -------------------------------------------------------------------------------- /config-example.toml: -------------------------------------------------------------------------------- 1 | [account] 2 | qq = 100000 3 | password = "" 4 | 5 | [minecraft] 6 | default = "mc.hypixel.net" 7 | 8 | [minecraft.servers] 9 | hypixel = "mc.hypixel.net" 10 | 11 | [bilibili] 12 | lives = [123] 13 | 14 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": { 3 | "qq": 100000, 4 | "password": "" 5 | }, 6 | "minecraft": { 7 | "default": { 8 | "host": "", 9 | "port": 25565 10 | }, 11 | "servers": { 12 | "example": { 13 | "host": "", 14 | "port": 25565 15 | } 16 | } 17 | }, 18 | "dict": { 19 | "aliases": { 20 | }, 21 | "userAliases": { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /destinybot-api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.kotlin.jvm' 4 | } 5 | 6 | group 'net.origind.destinybot.api' 7 | version project["api.version"] 8 | 9 | sourceCompatibility = targetCompatibility = "16" 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 17 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.0' 18 | 19 | implementation 'org.slf4j:slf4j-api:2.0.0-alpha1' 20 | implementation 'com.electronwill.night-config:core:3.6.4' 21 | } 22 | 23 | test { 24 | useJUnitPlatform() 25 | } 26 | 27 | compileKotlin { 28 | kotlinOptions.jvmTarget = "16" 29 | } 30 | compileTestKotlin { 31 | kotlinOptions.jvmTarget = "16" 32 | } 33 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/module-info.java: -------------------------------------------------------------------------------- 1 | module destinybot.api { 2 | requires java.base; 3 | requires kotlin.stdlib; 4 | requires kotlinx.coroutines.core.jvm; 5 | requires org.slf4j; 6 | requires com.electronwill.nightconfig.core; 7 | 8 | exports net.origind.destinybot.api.cache; 9 | exports net.origind.destinybot.api.command; 10 | exports net.origind.destinybot.api.plugin; 11 | exports net.origind.destinybot.api.util; 12 | exports net.origind.destinybot.api.timer; 13 | } 14 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/cache/Cache.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.cache 2 | 3 | open class Cache { 4 | } 5 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/cache/CacheManager.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.cache 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Paths 5 | 6 | const val CACHEDIR_TAG = """Signature: 8a477f597d28d172789f06886806bc55 7 | # This file is a cache directory tag created by Destiny Bot. 8 | # For information about cache directory tags, see: 9 | # http://www.brynosaurus.com/cachedir/ 10 | """ 11 | 12 | class CacheManager(val folder: String = "cache") { 13 | val path = Paths.get(folder) 14 | 15 | init { 16 | if (!Files.isDirectory(path)) { 17 | Files.createDirectory(path) 18 | } 19 | 20 | if (Files.notExists(path.resolve("CACHEDIR.TAG"))) { 21 | Files.writeString(path.resolve("CACHEDIR.TAG"), CACHEDIR_TAG) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/cache/ETagJsonCache.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.cache 2 | 3 | /** 4 | * Usually used in REST api return 5 | */ 6 | open class ETagJsonCache : Cache() { 7 | } 8 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/AbstractCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | abstract class AbstractCommand(final override val name: String): Command { 4 | val subcommandMap = mutableMapOf() 5 | 6 | override var permission: String = "destinybot.$name" 7 | 8 | override val arguments = mutableListOf>() 9 | 10 | val container by lazy { ArgumentContainer(arguments) } 11 | 12 | override val aliases: List = emptyList() 13 | override val examples: List = emptyList() 14 | 15 | override var description: String? = null 16 | 17 | override val argumentContainer: ArgumentContainer by lazy { ArgumentContainer(arguments) } 18 | 19 | override fun hasSubcommand(name: String): Boolean = subcommandMap.containsKey(name) 20 | 21 | override fun getSubcommand(name: String): Command? = 22 | subcommandMap[name] 23 | 24 | override fun getSubcommands(): Collection = 25 | subcommandMap.values 26 | 27 | fun registerSubcommand(command: Command) { 28 | subcommandMap[command.name] = command 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/AbstractCustomCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | abstract class AbstractCustomCommand(name: String) : AbstractCommand(name), CustomCommand { 4 | override suspend fun parse( 5 | main: String, 6 | parser: CommandParser, 7 | executor: CommandExecutor, 8 | context: CommandContext 9 | ) { 10 | try { 11 | if (parser.hasMore()) { 12 | val sub = parser.take(false) 13 | if (hasSubcommand(sub)) { 14 | parser.take() 15 | getSubcommand(sub)?.parse(parser, executor, context) 16 | } else { 17 | argumentContainer.parse(parser) 18 | execute(main, argumentContainer, executor, context) 19 | } 20 | } else { 21 | argumentContainer.parse(parser) 22 | execute(main, argumentContainer, executor, context) 23 | } 24 | } catch (e: ArgumentParseException) { 25 | executor.sendMessage("命令参数解析错误: ${e.localizedMessage}") 26 | } 27 | } 28 | 29 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) {} 30 | 31 | abstract suspend fun execute(main: String, argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) 32 | } 33 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/ArgumentContainer.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | class ArgumentContainer(val arguments: List>) { 4 | val argumentContextMap: Map> 5 | val requiredArgumentsToParse: Int 6 | private val argumentMap = mutableMapOf() 7 | 8 | var deque: ArrayDeque> 9 | 10 | val helpText: String 11 | 12 | init { 13 | if (!arguments.toSet().containsAll(arguments)) 14 | throw IllegalArgumentException("Argument name is not unique: " + arguments.intersect(arguments.toSet()).joinToString(", ") { it.name }) 15 | 16 | argumentContextMap = arguments.associateBy { it.name } 17 | deque = ArrayDeque(arguments) 18 | requiredArgumentsToParse = arguments.count { !it.optional } 19 | 20 | helpText = buildString { 21 | appendLine(arguments.joinToString(" ") { 22 | if (it.optional) "[${it.name}]" else "(${it.name})" 23 | }) 24 | arguments.filter { it.description != null }.forEach { 25 | appendLine("${it.name}: ${it.description}") 26 | } 27 | }.trim() 28 | } 29 | 30 | fun parse(parser: CommandParser) { 31 | argumentMap.clear() 32 | deque = ArrayDeque(arguments) 33 | var parsedRequiredArguments = 0 34 | var internal: String? = null 35 | while (deque.isNotEmpty() && (parser.hasMore() || internal != null)) { 36 | val arg = deque.first() 37 | if (!arg.optional) { 38 | if (internal != null) { 39 | argumentMap[arg.name] = 40 | arg.type.parse(internal) ?: throw ArgumentParseException("无法解析 ${arg.name} 参数") 41 | internal = null 42 | } else { 43 | argumentMap[arg.name] = 44 | arg.type.parse(parser.take()) ?: throw ArgumentParseException("无法解析 ${arg.name} 参数") 45 | } 46 | parsedRequiredArguments++ 47 | } else { 48 | if (internal != null) { 49 | val parsed = arg.type.parse(internal) 50 | if (parsed != null) { 51 | argumentMap[arg.name] = parsed 52 | internal = null 53 | } 54 | } else { 55 | val str = parser.take() 56 | val parsed = arg.type.parse(str) 57 | if (parsed != null) { 58 | argumentMap[arg.name] = parsed 59 | } else { 60 | internal = str 61 | } 62 | } 63 | } 64 | deque.removeFirst() 65 | } 66 | if (parsedRequiredArguments != requiredArgumentsToParse) { 67 | validate() 68 | } 69 | } 70 | 71 | fun validate() { 72 | val invalidArguments = mutableListOf>() 73 | 74 | for (argument in arguments) { 75 | if (!(argumentMap.containsKey(argument.name) || argument.optional)) { 76 | invalidArguments += argument 77 | } 78 | } 79 | 80 | if (invalidArguments.isNotEmpty()) { 81 | throw ArgumentParseException("缺失必填参数 " + invalidArguments.joinToString { it.name }) 82 | } 83 | } 84 | 85 | @Suppress("UNCHECKED_CAST") 86 | fun getArgument(name: String): T = argumentMap[name] as T 87 | 88 | fun hasArgument(name: String) = argumentMap.containsKey(name) 89 | } 90 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/ArgumentContext.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | data class ArgumentContext(val name: String, val type: ArgumentType, val optional: Boolean = false, val description: String? = null) { 4 | override fun equals(other: Any?): Boolean { 5 | if (this === other) return true 6 | if (other !is ArgumentContext<*>) return false 7 | 8 | if (name != other.name) return false 9 | 10 | return true 11 | } 12 | 13 | override fun hashCode(): Int { 14 | return name.hashCode() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/ArgumentParseException.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | class ArgumentParseException(message: String? = null) : Exception(message) 4 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/ArgumentType.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | interface ArgumentType { 4 | val clazz: Class 5 | 6 | @Throws(ArgumentParseException::class) 7 | fun parse(literal: String): T 8 | } 9 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/ArgumentTypes.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | object BooleanArgument: ArgumentType { 4 | override val clazz: Class = Boolean::class.java 5 | 6 | override fun parse(literal: String): Boolean = literal.toBooleanStrictOrNull() ?: throw ArgumentParseException("Must be true or false") 7 | } 8 | 9 | object StringArgument: ArgumentType { 10 | override val clazz: Class = String::class.java 11 | 12 | override fun parse(literal: String): String = literal 13 | } 14 | 15 | object IntArgument: ArgumentType { 16 | override val clazz: Class = Int::class.java 17 | 18 | override fun parse(literal: String): Int = literal.toIntOrNull() ?: throw ArgumentParseException("Not a valid int") 19 | } 20 | 21 | object LongArgument: ArgumentType { 22 | override val clazz: Class = Long::class.java 23 | 24 | override fun parse(literal: String): Long = literal.toLongOrNull() ?: throw ArgumentParseException("Not a valid long") 25 | } 26 | 27 | object QQArgument: ArgumentType { 28 | override val clazz: Class = Long::class.java 29 | 30 | override fun parse(literal: String): Long = literal.removePrefix("@").toLongOrNull() ?: throw ArgumentParseException("Not a valid qq number") 31 | } 32 | 33 | object DoubleArgument: ArgumentType { 34 | override val clazz: Class = Double::class.java 35 | 36 | override fun parse(literal: String): Double = literal.toDoubleOrNull() ?: throw ArgumentParseException("Not a valid double") 37 | } 38 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/Command.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | interface Command { 4 | /** 5 | * 命令的名称 6 | */ 7 | val name: String 8 | 9 | /** 10 | * 命令权限 11 | */ 12 | val permission: String 13 | 14 | /** 15 | * 命令描述 16 | * TODO i18n 17 | */ 18 | val description: String? 19 | 20 | /** 21 | * 启用默认帮助(/xxx help) 22 | */ 23 | val helpEnabled: Boolean 24 | get() = true 25 | 26 | val arguments: List> 27 | 28 | val argumentContainer: ArgumentContainer 29 | get() = ArgumentContainer(arguments) 30 | 31 | /** 32 | * 别名 33 | */ 34 | val aliases: List get() = emptyList() 35 | 36 | /** 37 | * 用例 38 | */ 39 | val examples: List get() = emptyList() 40 | 41 | /** 42 | * 用来代替 Lazy 43 | */ 44 | suspend fun init() {} 45 | 46 | /** 47 | * 获取一个子命令 48 | */ 49 | fun getSubcommand(name: String): Command? 50 | 51 | fun getSubcommands(): Collection 52 | 53 | /** 54 | * 是否存在子命令 55 | * 若为空则永远为真 56 | */ 57 | fun hasSubcommand(name: String): Boolean = 58 | name.isEmpty() 59 | 60 | /** 61 | * 解析命令并执行 62 | */ 63 | suspend fun parse(parser: CommandParser, executor: CommandExecutor, context: CommandContext) { 64 | try { 65 | if (parser.hasMore()) { 66 | val sub = parser.take(false) 67 | if ((sub == "help" || sub == "?") && helpEnabled) { 68 | executor.sendMessage(getHelp()) 69 | } else if (hasSubcommand(sub)) { 70 | parser.take() 71 | getSubcommand(sub)?.parse(parser, executor, context) 72 | } else { 73 | argumentContainer.parse(parser) 74 | if (executor.hasPermission(permission)) 75 | execute(argumentContainer, executor, context) 76 | else 77 | executor.sendMessage("你无权执行该命令") 78 | } 79 | } else { 80 | argumentContainer.parse(parser) 81 | if (executor.hasPermission(permission)) 82 | execute(argumentContainer, executor, context) 83 | else 84 | executor.sendMessage("你无权执行该命令") 85 | } 86 | } catch (e: ArgumentParseException) { 87 | executor.sendMessage("命令参数解析错误: ${e.localizedMessage}") 88 | } 89 | } 90 | 91 | /** 92 | * 执行命令 93 | * @param argument 解析过的参数 94 | * @param executor 命令执行者 95 | * @param context 命令上下文 96 | */ 97 | suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) 98 | 99 | /** 100 | * 帮助 101 | */ 102 | fun getHelp(): String = buildString { 103 | append(name) 104 | if (description != null) 105 | append(" - ").appendLine(description) 106 | else 107 | appendLine() 108 | 109 | if (aliases.isNotEmpty()) 110 | appendLine("别名: [${aliases.joinToString()}]") 111 | 112 | getSubcommands().forEach { command -> 113 | appendLine("子命令 ${command.name}:") 114 | appendLine(command.getHelp()) 115 | } 116 | 117 | if (arguments.isNotEmpty()) { 118 | appendLine("参数: $name " + argumentContainer.helpText) 119 | } else { 120 | appendLine("该命令没有任何参数。") 121 | } 122 | 123 | examples.forEach(this::appendLine) 124 | }.trim() 125 | } 126 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CommandContext.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | data class CommandContext(val senderId: Long, val subjectId: Long, val message: String, val time: Long) 4 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CommandException.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | class CommandException(message: String? = null) : Exception(message) { 4 | } 5 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CommandExecutor.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | interface CommandExecutor { 4 | fun hasPermission(node: String): Boolean 5 | fun sendMessage(text: String) 6 | fun sendPrivateMessage(text: String) = sendMessage(text) 7 | } 8 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CommandManager.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Job 5 | 6 | interface CommandManager : CoroutineScope { 7 | val commands: List 8 | val helpText: String 9 | 10 | fun init(): Job 11 | 12 | fun buildCache() 13 | 14 | fun register(command: Command) 15 | 16 | fun parse(command: String, executor: CommandExecutor, context: CommandContext) 17 | } 18 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CommandParser.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | class CommandParser(val command: String) { 4 | var internal = command 5 | 6 | init { 7 | trim() // TODO Config section 8 | } 9 | 10 | fun trim() { 11 | internal = internal.trim() 12 | } 13 | 14 | fun hasMore() = 15 | internal.trim().isNotBlank() 16 | 17 | /** 18 | * Take a string argument and jump 19 | */ 20 | fun take(move: Boolean = true): String { 21 | val index = internal.indexOf(' ') 22 | if (index == -1) { 23 | if (internal.isBlank()) throw IndexOutOfBoundsException("Command parser is complete") 24 | val temp = internal.trim() 25 | if (move) 26 | internal = "" 27 | return temp 28 | } 29 | val temp = internal.substring(0, index) 30 | if (move) 31 | internal = internal.substring(index + 1).trim() 32 | return temp 33 | } 34 | 35 | fun take(n: Int): Array { 36 | val arr = Array(n) { "" } 37 | for (i in 0 until n) 38 | arr[i] = take() 39 | return arr 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CommandSpec.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | class CommandSpec: Command { 4 | override var name: String = "" 5 | 6 | val subcommandMap = mutableMapOf() 7 | override var permission: String = "destinybot.$name" 8 | 9 | override val arguments = mutableListOf>() 10 | 11 | val container by lazy { ArgumentContainer(arguments) } 12 | var executor: (ArgumentContainer, CommandExecutor, CommandContext) -> Unit = { _, _, _ -> } 13 | 14 | override val aliases: List = emptyList() 15 | override val examples: List = emptyList() 16 | 17 | override var description: String? = null 18 | 19 | override val argumentContainer: ArgumentContainer by lazy { ArgumentContainer(arguments) } 20 | 21 | override fun getSubcommand(name: String): Command? = 22 | subcommandMap[name] 23 | 24 | override fun getSubcommands(): Collection = 25 | subcommandMap.values 26 | 27 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 28 | executor(argument, executor, context) 29 | } 30 | 31 | fun registerSubcommand(command: CommandSpec) { 32 | subcommandMap[command.name] = command 33 | } 34 | 35 | fun subcommand(init1: CommandSpec.() -> Unit): CommandSpec { 36 | val spec = CommandSpec() 37 | spec.init1() 38 | registerSubcommand(spec) 39 | return spec 40 | } 41 | 42 | fun argument(name: String, type: ArgumentType<*>, description: String? = null, optional: Boolean = false) { 43 | arguments += ArgumentContext(name, type, optional, description) 44 | } 45 | } 46 | 47 | fun command(init1: CommandSpec.() -> Unit): CommandSpec { 48 | val spec = CommandSpec() 49 | spec.init1() 50 | return spec 51 | } 52 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/ConsoleCommandExecutor.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | object ConsoleCommandExecutor : CommandExecutor { 4 | // Console has all permissions 5 | override fun hasPermission(node: String): Boolean = true 6 | 7 | override fun sendMessage(text: String) { 8 | println(text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/CustomCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | interface CustomCommand: Command { 4 | suspend fun parse(main: String, parser: CommandParser, executor: CommandExecutor, context: CommandContext) 5 | } 6 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/command/UserCommandExecutor.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.command 2 | 3 | abstract class UserCommandExecutor : CommandExecutor { 4 | abstract fun groupContains(qq: Long): Boolean 5 | 6 | abstract fun sendImage(image: ByteArray) 7 | 8 | abstract fun sendPrivateImage(image: ByteArray) 9 | } 10 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/package-info.java: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api; 2 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/plugin/Plugin.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.plugin 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | import net.origind.destinybot.api.command.CommandManager 5 | 6 | interface Plugin { 7 | val name: String 8 | val version: String 9 | 10 | fun init() 11 | 12 | fun reloadConfig(config: Config) 13 | 14 | suspend fun reload() {} 15 | 16 | fun registerCommand(manager: CommandManager) 17 | } 18 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/plugin/PluginContainer.java: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.plugin; 2 | 3 | public class PluginContainer { 4 | } 5 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/timer/TimedTask.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.timer 2 | 3 | import java.time.Duration 4 | import java.time.LocalDateTime 5 | 6 | class TimedTask(val task: suspend () -> Unit, var interval: Duration, var lastExecuted: LocalDateTime = LocalDateTime.MIN) { 7 | } 8 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/timer/TimerManager.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.timer 2 | 3 | import kotlinx.coroutines.* 4 | import java.time.LocalDateTime 5 | import java.time.format.DateTimeFormatter 6 | import java.util.concurrent.Executors 7 | 8 | val LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 9 | 10 | object TimerManager { 11 | private val scope: CoroutineScope 12 | private val taskScheduler: CoroutineDispatcher 13 | val tasks = mutableMapOf() 14 | private val disabledTasks = hashSetOf() 15 | 16 | init { 17 | taskScheduler = Executors.newFixedThreadPool(8).asCoroutineDispatcher() 18 | scope = CoroutineScope(taskScheduler) 19 | } 20 | 21 | fun schedule(name: String, task: TimedTask) { 22 | tasks[name] = task 23 | } 24 | 25 | fun disable(name: String) { 26 | disabledTasks += name 27 | } 28 | 29 | fun enable(name: String) { 30 | disabledTasks -= name 31 | } 32 | 33 | fun run() { 34 | for ((name, task) in tasks) { 35 | scope.launch { 36 | while (name !in disabledTasks) { 37 | task.task() 38 | task.lastExecuted = LocalDateTime.now() 39 | delay(task.interval.toMillis()) 40 | } 41 | } 42 | } 43 | } 44 | 45 | fun cancel() { 46 | scope.cancel() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/util/DurationHelper.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.util 2 | 3 | import java.time.Duration 4 | 5 | fun Duration.toLocalizedString() = buildString { 6 | val duration = this@toLocalizedString 7 | val days = duration.toDaysPart() 8 | val hours = duration.toHoursPart() 9 | val minutes = duration.toMinutesPart() 10 | val seconds = duration.toSecondsPart() 11 | if (days > 0) append("$days 天 ") 12 | if (hours > 0) append("$hours 小时 ") 13 | if (minutes > 0) append("$minutes 分 ") 14 | if (seconds > 0) append("$seconds 秒") 15 | }.trim() 16 | -------------------------------------------------------------------------------- /destinybot-api/src/main/kotlin/net/origind/destinybot/api/util/ExceptionHelper.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.api.util 2 | 3 | import java.io.PrintWriter 4 | import java.io.StringWriter 5 | 6 | fun Throwable.joinToString(): String { 7 | val writer = StringWriter() 8 | this.printStackTrace(PrintWriter(writer)) 9 | writer.close() 10 | return writer.toString() 11 | } 12 | -------------------------------------------------------------------------------- /destinybot-core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.kotlin.jvm' 4 | } 5 | 6 | group 'net.origind.destinybot' 7 | version project["core.version"] 8 | 9 | sourceCompatibility = targetCompatibility = "16" 10 | 11 | repositories { 12 | mavenCentral() 13 | maven { url 'https://repo.opencollab.dev/maven-releases' } 14 | } 15 | 16 | dependencies { 17 | implementation project(":destinybot-api") 18 | implementation project(":destinybot-features") 19 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 20 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.4' 21 | implementation 'com.electronwill.night-config:toml:3.6.4' 22 | 23 | implementation group: 'com.squareup.moshi', name: 'moshi', version: '1.9.2' 24 | implementation group: 'com.squareup.moshi', name: 'moshi-kotlin', version: '1.9.2' 25 | 26 | runtimeOnly "net.mamoe:mirai-core:$miraiVersion" 27 | api "net.mamoe:mirai-core-api:$miraiVersion" 28 | 29 | implementation 'ch.qos.logback:logback-classic:1.3.0-beta0' 30 | 31 | implementation 'it.unimi.dsi:fastutil:8.5.6' 32 | implementation 'com.projecturanus:suffixtree:1.0' 33 | } 34 | 35 | test { 36 | useJUnitPlatform() 37 | } 38 | 39 | compileKotlin { 40 | kotlinOptions.jvmTarget = "16" 41 | } 42 | compileTestKotlin { 43 | kotlinOptions.jvmTarget = "16" 44 | } 45 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/module-info.java: -------------------------------------------------------------------------------- 1 | module destinybot.core { 2 | requires java.base; 3 | requires kotlinx.coroutines.core.jvm; 4 | requires org.slf4j; 5 | requires java.desktop; 6 | requires mirai.core.api.jvm; 7 | requires kotlin.stdlib; 8 | requires it.unimi.dsi.fastutil; 9 | requires suffixtree; 10 | 11 | requires transitive destinybot.api; 12 | requires transitive destinybot.features; 13 | 14 | requires com.squareup.moshi.kotlin; 15 | requires com.squareup.moshi; 16 | requires com.electronwill.nightconfig.core; 17 | requires com.electronwill.nightconfig.toml; 18 | requires kotlinx.serialization.json; 19 | requires kotlinx.serialization.core; 20 | 21 | exports net.origind.destinybot.core; 22 | } 23 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/BotConfig.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core 2 | 3 | data class BotConfig(var sudoEnabledGroups: List = listOf()) 4 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/TestBot.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core 2 | 3 | import com.electronwill.nightconfig.core.file.FileConfig 4 | import com.electronwill.nightconfig.toml.TomlFormat 5 | import net.origind.destinybot.features.minecraft.MinecraftConfig 6 | import java.nio.file.Paths 7 | 8 | fun main() { 9 | val config = FileConfig.of(Paths.get("config.toml").toAbsolutePath(), TomlFormat.instance()) 10 | config.load() 11 | println(Paths.get("config.toml").toAbsolutePath()) 12 | println(MinecraftConfig(config)) 13 | } 14 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/AdminCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.mamoe.mirai.contact.Member 4 | import net.mamoe.mirai.contact.getMemberOrFail 5 | import net.origind.destinybot.api.command.* 6 | 7 | object AdminCommand : AbstractCommand("/admin") { 8 | init { 9 | permission = "op.admin" 10 | registerSubcommand(Enable) 11 | registerSubcommand(Disable) 12 | } 13 | 14 | object Enable : AbstractCommand("enable") { 15 | init { 16 | permission = "op.admin.enable" 17 | arguments += ArgumentContext("id", QQArgument) 18 | } 19 | 20 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 21 | val id: Long = argument.getArgument("id") 22 | ((executor as MiraiUserCommandExecutor).user as Member).group.getMemberOrFail(id).modifyAdmin(true) 23 | executor.sendMessage("已将 $id 设为管理员") 24 | } 25 | } 26 | 27 | object Disable : AbstractCommand("disable") { 28 | init { 29 | permission = "op.admin.disable" 30 | arguments += ArgumentContext("id", QQArgument) 31 | } 32 | 33 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 34 | val id: Long = argument.getArgument("id") 35 | ((executor as MiraiUserCommandExecutor).user as Member).group.getMemberOrFail(argument.getArgument("id")).modifyAdmin(false) 36 | executor.sendMessage("已移除 $id 的管理员") 37 | } 38 | } 39 | 40 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 41 | executor.sendMessage(getHelp()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/AnnouncementCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import kotlinx.coroutines.flow.map 6 | import kotlinx.coroutines.flow.toList 7 | import kotlinx.serialization.builtins.ListSerializer 8 | import kotlinx.serialization.json.Json 9 | import net.mamoe.mirai.contact.Member 10 | import net.mamoe.mirai.contact.announcement.OfflineAnnouncement 11 | import net.origind.destinybot.api.command.* 12 | import net.origind.destinybot.core.util.ContactAdapter 13 | import net.origind.destinybot.core.util.decodeGZIPBase64 14 | import net.origind.destinybot.core.util.toGZIPCompressedBase64Encoded 15 | 16 | object AnnouncementCommand: AbstractCommand("/announcement") { 17 | val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(ContactAdapter).build() 18 | 19 | init { 20 | permission = "admin.announcement" 21 | registerSubcommand(Restore) 22 | registerSubcommand(Backup) 23 | } 24 | 25 | object Restore: AbstractCommand("restore") { 26 | init { 27 | permission = "admin.announcement.restore" 28 | arguments += ArgumentContext("data", StringArgument) 29 | } 30 | 31 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 32 | val data = argument.getArgument("data").trim().decodeGZIPBase64() 33 | if (executor is MiraiUserCommandExecutor && executor.user is Member) { 34 | val group = executor.user.group 35 | if (group.botPermission.level < 1) { 36 | executor.sendMessage("不是群管理无法恢复。") 37 | return 38 | } 39 | val announcements = Json.decodeFromString(ListSerializer(OfflineAnnouncement.serializer()), data) 40 | announcements.forEach { group.announcements.publish(it) } 41 | } else { 42 | executor.sendMessage("不是群管理无法恢复。") 43 | } 44 | } 45 | } 46 | 47 | object Backup: AbstractCommand("backup") { 48 | init { 49 | permission = "admin.announcement.backup" 50 | } 51 | 52 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 53 | if (executor is MiraiUserCommandExecutor && executor.user is Member) { 54 | val group = executor.user.group 55 | val data = Json.encodeToString(ListSerializer(OfflineAnnouncement.serializer()), group.announcements.asFlow().map { 56 | OfflineAnnouncement.create( 57 | it.content, 58 | it.parameters 59 | ) 60 | }.toList()) 61 | executor.sendMessage(data.toGZIPCompressedBase64Encoded()) 62 | } else { 63 | executor.sendMessage("请在群聊中调用。") 64 | } 65 | } 66 | } 67 | 68 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 69 | executor.sendMessage(getHelp()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/CommandManager.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import com.projecturanus.suffixtree.GeneralizedSuffixTree 4 | import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap 6 | import it.unimi.dsi.fastutil.ints.Int2ObjectMaps 7 | import kotlinx.coroutines.* 8 | import net.origind.destinybot.api.command.* 9 | import net.origind.destinybot.api.command.CommandManager 10 | import net.origind.destinybot.api.util.joinToString 11 | import java.util.concurrent.Executors 12 | import kotlin.coroutines.CoroutineContext 13 | 14 | fun checkCommand(command: Command) { 15 | if (command.name.isBlank()) throw CommandException("命令没有名称") 16 | } 17 | 18 | object CommandManager: CoroutineScope, CommandManager { 19 | override val commands = mutableListOf() 20 | override var helpText = "" 21 | 22 | private val commandMap = mutableMapOf() 23 | private val customCommands = mutableListOf() 24 | private var commandNameCache: Map = emptyMap() 25 | private var commandIndexCache: Int2ObjectMap = Int2ObjectMaps.EMPTY_MAP as Int2ObjectMap 26 | private var searchTree: GeneralizedSuffixTree = GeneralizedSuffixTree() 27 | 28 | override fun init() = 29 | launch { 30 | for (command in commands.toList()) { 31 | command.init() 32 | } 33 | for (customCommand in customCommands.toList()) { 34 | customCommand.init() 35 | } 36 | } 37 | 38 | 39 | 40 | override fun buildCache() { 41 | searchTree = GeneralizedSuffixTree() 42 | commandNameCache = commands.associateBy { it.name } 43 | commandIndexCache = Int2ObjectArrayMap() 44 | var index = 0 45 | commands.forEach { command -> 46 | index++ 47 | commandIndexCache[index] = command 48 | searchTree.put(command.name, index) 49 | command.aliases.forEach { 50 | index++ 51 | commandIndexCache[index] = command 52 | searchTree.put(it, index) 53 | } 54 | commandNameCache = commandNameCache + command.aliases.associateWith { command } 55 | helpText = commands.joinToString("\n\n") { it.getHelp() } 56 | } 57 | } 58 | 59 | override fun register(command: Command) { 60 | checkCommand(command) 61 | if (command is CustomCommand) { 62 | customCommands += command 63 | } else { 64 | commands += command 65 | commandMap[command.name] = command 66 | } 67 | } 68 | 69 | override fun parse(command: String, executor: CommandExecutor, context: CommandContext) { 70 | val parser = CommandParser(command) 71 | val main = parser.take() 72 | val handler = CoroutineExceptionHandler { coroutineContext, throwable -> 73 | executor.sendMessage("执行出现错误: " + throwable.joinToString()) 74 | } 75 | customCommands.forEach { 76 | launch(handler) { withTimeout(10_000) { it.parse(main, parser, executor, context) } } 77 | } 78 | if (commandNameCache.containsKey(main)) { 79 | launch(handler) { withTimeout(10_000) { commandNameCache[main]?.parse(parser, executor, context) } } 80 | } else { 81 | // val top = FuzzySearch.extractTop(main, commandNameCache.keys, 1, 90) 82 | // if (top.isNotEmpty()) { 83 | // executor.sendMessage("未找到命令 $main, 您要找的是不是 ${top.first().string}(匹配度 ${top.first().score}%)") 84 | // } 85 | 86 | } 87 | } 88 | 89 | override val coroutineContext: CoroutineContext = Executors.newCachedThreadPool().asCoroutineDispatcher() 90 | } 91 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/ConfigCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.* 4 | import net.origind.destinybot.core.DestinyBot 5 | 6 | object ConfigCommand: AbstractCommand("/config") { 7 | init { 8 | permission = "admin.config" 9 | registerSubcommand(GetCommand) 10 | registerSubcommand(SetCommand) 11 | registerSubcommand(DeleteCommand) 12 | } 13 | 14 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 15 | executor.sendMessage("用法: /config get [node]\n/config set (node) (value)\n/config delete (node)") 16 | } 17 | 18 | object GetCommand : AbstractCommand("get") { 19 | init { 20 | permission = "admin.config.get" 21 | arguments += ArgumentContext("node", StringArgument, true) 22 | } 23 | 24 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 25 | val node = argument.getArgument("node") ?: "" 26 | if (DestinyBot.config.contains(node)) 27 | executor.sendMessage(DestinyBot.config.get(node).toString()) 28 | else 29 | executor.sendMessage("配置中不存在 $node 节点") 30 | } 31 | 32 | } 33 | 34 | object SetCommand : AbstractCommand("set") { 35 | init { 36 | permission = "admin.config.set" 37 | arguments += ArgumentContext("node", StringArgument) 38 | arguments += ArgumentContext("value", StringArgument) 39 | } 40 | 41 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 42 | val node = argument.getArgument("node") 43 | val value = argument.getArgument("value") 44 | 45 | if (DestinyBot.config.contains(node)) { 46 | val current = DestinyBot.config.get(node) 47 | 48 | if (current.javaClass != String::class.java) { 49 | executor.sendMessage("暂不支持设定 ${current.javaClass.name} 类型的节点") 50 | return 51 | } else { 52 | executor.sendMessage("原内容为 $current") 53 | } 54 | } 55 | DestinyBot.config.set(node, value) 56 | DestinyBot.config.save() 57 | DestinyBot.reloadConfig() 58 | executor.sendMessage("已将 $node 设置为 $value") 59 | } 60 | } 61 | 62 | object DeleteCommand : AbstractCommand("delete") { 63 | init { 64 | permission = "admin.config.remove" 65 | arguments += ArgumentContext("node", StringArgument) 66 | } 67 | 68 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 69 | val node = argument.getArgument("node") 70 | 71 | DestinyBot.config.remove(node) 72 | DestinyBot.config.save() 73 | DestinyBot.reloadConfig() 74 | executor.sendMessage("已移除 $node") 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/DeopCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.* 4 | import net.origind.destinybot.core.DestinyBot 5 | 6 | object DeopCommand: AbstractCommand("/deop") { 7 | init { 8 | arguments += ArgumentContext("id", QQArgument) 9 | } 10 | 11 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 12 | if (executor is MiraiUserCommandExecutor && DestinyBot.ops.contains(executor.user.id)) { 13 | val id = argument.getArgument("id") 14 | DestinyBot.ops -= id 15 | DestinyBot.config.set>("bot.ops", DestinyBot.ops) 16 | DestinyBot.config.save() 17 | executor.sendMessage("De-Opped $id") 18 | } else { 19 | executor.sendMessage("无权执行该命令。") 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/DistributionCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.mamoe.mirai.contact.Member 4 | import net.mamoe.mirai.contact.MemberPermission 5 | import net.origind.destinybot.api.command.* 6 | import java.nio.charset.StandardCharsets 7 | 8 | object DistributionCommand: AbstractCommand("分配头衔") { 9 | init { 10 | description = "头衔大分配" 11 | arguments += ArgumentContext("rank", StringArgument, false, "要设置的头衔,%1来替换index") 12 | arguments += ArgumentContext("shuffle", BooleanArgument, false, "是否 shuffle") 13 | 14 | permission = "admin.ranking.set" 15 | } 16 | 17 | 18 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 19 | val specialTitle = argument.getArgument("rank") 20 | val shuffle = if (argument.hasArgument("shuffle")) argument.getArgument("shuffle") else false 21 | 22 | if (executor is MiraiUserCommandExecutor && executor.user is Member) { 23 | val group = executor.user.group 24 | if (group.botPermission != MemberPermission.OWNER) { 25 | executor.sendMessage("机器人不是群主。") 26 | return 27 | } 28 | 29 | val iterator = if (shuffle) group.members.shuffled().withIndex() else group.members.withIndex() 30 | 31 | for ((i, member) in iterator) { 32 | if (member !in group) { 33 | executor.sendMessage("要设置的成员不在群中") 34 | return 35 | } 36 | val formatted = specialTitle.format(i) 37 | if (formatted.toByteArray(StandardCharsets.UTF_8).size > 18) { 38 | executor.sendMessage("请注意在 UTF-8 编码中大于 18 字节的头衔会被裁断。") 39 | } 40 | member.specialTitle = formatted 41 | } 42 | executor.sendMessage("设置成功") 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/GroupListCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import com.squareup.moshi.Types 4 | import net.mamoe.mirai.contact.Member 5 | import net.origind.destinybot.api.command.* 6 | import net.origind.destinybot.core.util.MemberData 7 | import net.origind.destinybot.core.util.toGZIPCompressedBase64Encoded 8 | import net.origind.destinybot.features.moshi 9 | 10 | object GroupListCommand : AbstractCommand("/名单") { 11 | override val aliases: List = listOf("/辛德勒的名单", "/拉清单", "/别看你今天闹得欢") 12 | 13 | init { 14 | arguments += ArgumentContext("isLong", BooleanArgument, true, "输出长格式") 15 | } 16 | 17 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 18 | if (executor is MiraiUserCommandExecutor && executor.user is Member) { 19 | val group = executor.user.group 20 | if (argument.hasArgument("isLong") && argument.getArgument("isLong")) { 21 | executor.sendMessage("本消息经过 GZIP 压缩并通过 Base64 编码,可以在 https://www.txtwizard.net/compression 解码。") 22 | executor.sendMessage( 23 | moshi.adapter>(Types.newParameterizedType(List::class.java, MemberData::class.java)) 24 | .toJson(group.members.map(::MemberData).toList()).toGZIPCompressedBase64Encoded() 25 | ) 26 | } else { 27 | executor.sendMessage("[${group.members.map { it.id }.joinToString()}]") 28 | } 29 | } else { 30 | executor.sendMessage("请在群聊中调用。") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/HelpCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | 8 | object HelpCommand: AbstractCommand("/dshelp") { 9 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 10 | executor.sendMessage(buildString { 11 | appendLine("欢迎使用 LG 的各种乱七八糟功能机器人 2.0 版。") 12 | appendLine("获取该帮助: /dshelp") 13 | appendLine("参数的帮助: 带()的为必填内容, []为选填内容") 14 | appendLine("如有任何问题[想被LG喷一顿] 请@你群中的LG") 15 | }) 16 | executor.sendMessage(CommandManager.helpText) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/KickCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.mamoe.mirai.contact.Member 4 | import net.mamoe.mirai.contact.getMemberOrFail 5 | import net.origind.destinybot.api.command.* 6 | 7 | object KickCommand : AbstractCommand("/kick") { 8 | init { 9 | permission = "op.kick" 10 | arguments += ArgumentContext("id", QQArgument) 11 | } 12 | 13 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 14 | if (executor is MiraiUserCommandExecutor && executor.user is Member) { 15 | val id: Long = argument.getArgument("id") 16 | executor.user.group.getMemberOrFail(id).kick("") 17 | executor.sendMessage("已将 $id 移出本群") 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/MemberJoinRequestCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.mamoe.mirai.event.events.MemberJoinRequestEvent 4 | import net.origind.destinybot.api.command.* 5 | import java.util.* 6 | 7 | object MemberJoinRequestCommand : AbstractCommand("/jr") { 8 | val events = LinkedList() 9 | init { 10 | registerSubcommand(Deny) 11 | registerSubcommand(Ignore) 12 | registerSubcommand(Accept) 13 | registerSubcommand(List) 14 | } 15 | 16 | object List : AbstractCommand("list") { 17 | init { 18 | permission = "op.jr.list" 19 | } 20 | 21 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 22 | executor.sendMessage(buildString { 23 | events.forEachIndexed { index, memberJoinRequestEvent -> 24 | appendLine("$index. ${memberJoinRequestEvent.fromNick} (${memberJoinRequestEvent.fromId}) 申请加入群 ${memberJoinRequestEvent.groupName} (${memberJoinRequestEvent.groupId}): ${memberJoinRequestEvent.message}") 25 | } 26 | }) 27 | } 28 | 29 | } 30 | 31 | object Deny : AbstractCommand("deny") { 32 | init { 33 | arguments += ArgumentContext("index", IntArgument) 34 | arguments += ArgumentContext("reason", StringArgument) 35 | permission = "op.jr.deny" 36 | } 37 | 38 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 39 | val event = events.getOrNull(argument.getArgument("index")) 40 | event?.reject(argument.getArgument("reason")) 41 | events.removeAt(argument.getArgument("index")) 42 | executor.sendMessage("已拒绝 ${event?.fromId} 的入群申请。") 43 | } 44 | } 45 | 46 | object Ignore : AbstractCommand("accept") { 47 | init { 48 | arguments += ArgumentContext("index", IntArgument) 49 | permission = "op.jr.ignore" 50 | } 51 | 52 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 53 | val event = events.getOrNull(argument.getArgument("index")) 54 | event?.ignore() 55 | events.removeAt(argument.getArgument("index")) 56 | executor.sendMessage("已忽略 ${event?.fromId} 的入群申请。") 57 | } 58 | } 59 | 60 | object Accept : AbstractCommand("accept") { 61 | init { 62 | arguments += ArgumentContext("index", IntArgument) 63 | permission = "op.jr.accept" 64 | } 65 | 66 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 67 | val event = events.getOrNull(argument.getArgument("index")) 68 | event?.accept() 69 | events.removeAt(argument.getArgument("index")) 70 | executor.sendMessage("已同意 ${event?.fromId} 的入群申请。") 71 | } 72 | } 73 | 74 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 75 | executor.sendMessage(getHelp()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/MiraiUserCommandExecutor.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import kotlinx.coroutines.launch 4 | import net.mamoe.mirai.contact.Contact.Companion.sendImage 5 | import net.mamoe.mirai.contact.Member 6 | import net.mamoe.mirai.contact.User 7 | import net.mamoe.mirai.message.data.Message 8 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 9 | import net.origind.destinybot.api.command.UserCommandExecutor 10 | import net.origind.destinybot.core.DestinyBot 11 | 12 | class MiraiUserCommandExecutor(val user: User) : UserCommandExecutor() { 13 | override fun groupContains(qq: Long): Boolean = user is Member && user.group.contains(qq) 14 | 15 | override fun sendImage(image: ByteArray) { 16 | if (image.isEmpty()) return 17 | user.launch { 18 | if (user is Member) { 19 | user.group.sendImage(image.toExternalResource("png")) 20 | } else { 21 | user.sendImage(image.toExternalResource("png")) 22 | } 23 | } 24 | } 25 | 26 | override fun sendPrivateImage(image: ByteArray) { 27 | if (image.isEmpty()) return 28 | user.launch { 29 | user.sendImage(image.toExternalResource("png")) 30 | } 31 | } 32 | 33 | override fun hasPermission(node: String): Boolean = 34 | if (node.startsWith("op.") || node.startsWith("admin.")) { 35 | DestinyBot.ops.contains(user.id) 36 | } 37 | else if (node.startsWith("admin.")) (user is Member && user.permission.level > 0) 38 | else true 39 | 40 | 41 | override fun sendMessage(text: String) { 42 | if (text.isBlank()) return 43 | user.launch { 44 | if (user is Member) { 45 | user.group.sendMessage(text.trim()) 46 | } else { 47 | user.sendMessage(text.trim()) 48 | } 49 | } 50 | } 51 | 52 | override fun sendPrivateMessage(text: String) { 53 | user.launch { user.sendMessage(text) } 54 | } 55 | 56 | fun sendMessage(miraiMsg: Message) { 57 | user.launch { 58 | if (user is Member) { 59 | user.group.sendMessage(miraiMsg) 60 | } else { 61 | user.sendMessage(miraiMsg) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/OpCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.* 4 | import net.origind.destinybot.core.DestinyBot 5 | 6 | object OpCommand: AbstractCommand("/op") { 7 | init { 8 | arguments += ArgumentContext("id", QQArgument) 9 | } 10 | 11 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 12 | if (executor is MiraiUserCommandExecutor && executor.user.id in DestinyBot.ops) { 13 | val id = argument.getArgument("id") 14 | DestinyBot.ops += id 15 | DestinyBot.config.set>("bot.ops", DestinyBot.ops) 16 | DestinyBot.config.save() 17 | executor.sendMessage("Opped $id") 18 | } else { 19 | executor.sendMessage("无权执行该命令。") 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/OpsCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | import net.origind.destinybot.core.DestinyBot 8 | 9 | object OpsCommand: AbstractCommand("/ops") { 10 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 11 | executor.sendMessage(DestinyBot.ops.joinToString()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/RankingCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.mamoe.mirai.contact.Member 4 | import net.mamoe.mirai.contact.MemberPermission 5 | import net.origind.destinybot.api.command.* 6 | import java.nio.charset.StandardCharsets 7 | 8 | object RankingCommand: AbstractCommand("设置头衔") { 9 | init { 10 | arguments += ArgumentContext("qq", LongArgument, false, "QQ号") 11 | arguments += ArgumentContext("rank", StringArgument, false, "要设置的头衔") 12 | 13 | permission = "admin.ranking.set" 14 | } 15 | 16 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 17 | val qq = argument.getArgument("qq") 18 | val specialTitle = argument.getArgument("rank") 19 | if (executor is MiraiUserCommandExecutor && executor.user is Member) { 20 | val group = executor.user.group 21 | if (group.botPermission != MemberPermission.OWNER) { 22 | executor.sendMessage("机器人不是群主。") 23 | return 24 | } 25 | if (specialTitle.toByteArray(StandardCharsets.UTF_8).size > 18) { 26 | executor.sendMessage("请注意在 UTF-8 编码中大于 18 字节的头衔会被裁断。") 27 | } 28 | if (qq !in group) { 29 | executor.sendMessage("要设置的成员不在群中") 30 | return 31 | } 32 | group[qq]!!.specialTitle = specialTitle 33 | executor.sendMessage("设置成功") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/ReloadCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | import net.origind.destinybot.core.DestinyBot 8 | 9 | object ReloadCommand: AbstractCommand("/reload") { 10 | init { 11 | permission = "admin.reload" 12 | } 13 | 14 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 15 | DestinyBot.config.load() 16 | DestinyBot.reloadConfig() 17 | for (plugin in DestinyBot.plugins) { 18 | plugin.reload() 19 | } 20 | executor.sendMessage("机器人已重载。") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/StatusCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | 8 | object StatusCommand: AbstractCommand("我不需要机器人") { 9 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/command/SudoCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.command 2 | 3 | import kotlinx.coroutines.delay 4 | import net.mamoe.mirai.contact.NormalMember 5 | import net.mamoe.mirai.contact.isOwner 6 | import net.origind.destinybot.api.command.AbstractCommand 7 | import net.origind.destinybot.api.command.ArgumentContainer 8 | import net.origind.destinybot.api.command.CommandContext 9 | import net.origind.destinybot.api.command.CommandExecutor 10 | 11 | object SudoCommand: AbstractCommand("/sudo") { 12 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 13 | if (executor is MiraiUserCommandExecutor && executor.user is NormalMember && executor.user.group.botPermission.isOwner()) { 14 | executor.user.modifyAdmin(true) 15 | executor.sendMessage("已将您临时设为管理员,五分钟后自动解除。") 16 | delay(5 * 60 * 1000L) 17 | executor.sendMessage("已自动解除 ${executor.user.id} 的管理员身份。") 18 | executor.user.modifyAdmin(false) 19 | } else { 20 | executor.sendMessage("无权执行该命令。") 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/task/CheckStreamerTask.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.task 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.awaitAll 6 | import kotlinx.coroutines.coroutineScope 7 | import net.origind.destinybot.core.DestinyBot 8 | import net.origind.destinybot.features.bilibili.LiveRoomInfo 9 | import net.origind.destinybot.features.bilibili.bilibiliConfig 10 | import net.origind.destinybot.features.bilibili.getLiveRoomInfo 11 | import net.origind.destinybot.features.bilibili.getUserInfo 12 | 13 | private val onlineStreamers = mutableSetOf() 14 | 15 | suspend fun checkStreamer() { 16 | var anyOnline = false 17 | 18 | val str = coroutineScope { 19 | buildString { 20 | val infos = bilibiliConfig.lives 21 | .map { async(Dispatchers.IO) { getLiveRoomInfo(it) } } 22 | .awaitAll() 23 | .toSet() 24 | 25 | val offline = infos 26 | .asSequence() 27 | .filter { it.live_status == 0 } 28 | .filter { onlineStreamers.any { info2 -> it.room_id == info2.room_id } } 29 | .toSet() 30 | offline.forEach { roomInfo -> 31 | appendLine("主播 " + roomInfo.title + " 下播了...") 32 | anyOnline = true 33 | } 34 | onlineStreamers.removeIf { offline.any { info -> it.room_id == info.room_id } } 35 | 36 | infos.asSequence() 37 | .filter { it.live_status == 1 } 38 | .filter { onlineStreamers.none { info -> it.room_id == info.room_id } } 39 | .forEach { roomInfo -> 40 | appendLine("你喜爱的主播 " + getUserInfo(roomInfo.uid)?.name() + " 正在直播: ${roomInfo.title}!https://live.bilibili.com/${roomInfo.room_id}") 41 | anyOnline = true 42 | onlineStreamers += roomInfo 43 | } 44 | }.trim() 45 | } 46 | if (!anyOnline) return 47 | for (id in bilibiliConfig.replyStreamersTo) { 48 | DestinyBot.bot.getGroup(id)?.sendMessage(str) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/util/AnnouncementAdapter.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.util 2 | 3 | import com.squareup.moshi.* 4 | import net.mamoe.mirai.contact.announcement.Announcement 5 | import net.mamoe.mirai.contact.announcement.AnnouncementParameters 6 | 7 | object AnnouncementAdapter : JsonAdapter() { 8 | @FromJson 9 | override fun fromJson(reader: JsonReader): Announcement? { 10 | TODO("Not yet implemented") 11 | } 12 | 13 | @ToJson 14 | override fun toJson(writer: JsonWriter, value: Announcement?) { 15 | writer.name("content").value(value?.content) 16 | writer.name("parameters") 17 | AnnouncementParametersAdapter.toJson(writer, value?.parameters) 18 | } 19 | } 20 | 21 | object AnnouncementParametersAdapter : JsonAdapter() { 22 | @FromJson 23 | override fun fromJson(reader: JsonReader): AnnouncementParameters? { 24 | return null 25 | } 26 | 27 | @ToJson 28 | override fun toJson(writer: JsonWriter, value: AnnouncementParameters?) { 29 | writer.beginObject() 30 | writer.name("isPinned").value(value?.isPinned) 31 | writer.name("requireConfirmation").value(value?.requireConfirmation) 32 | writer.name("sendToNewMember").value(value?.sendToNewMember) 33 | writer.name("showEditCard").value(value?.showEditCard) 34 | writer.name("showPopup").value(value?.showPopup) 35 | writer.endObject() 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/util/ConfigExtension.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.util 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | 5 | fun Config.getOrThrow(path: String, exception: () -> Throwable): T { 6 | val value = get(path) 7 | return value ?: throw exception() 8 | } -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/util/ContactAdapter.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.util 2 | 3 | import com.squareup.moshi.* 4 | import net.mamoe.mirai.contact.ContactOrBot 5 | import net.origind.destinybot.core.DestinyBot 6 | 7 | object ContactAdapter : JsonAdapter() { 8 | @FromJson 9 | override fun fromJson(reader: JsonReader): ContactOrBot? { 10 | return DestinyBot.bot.getStranger(reader.nextLong()) 11 | } 12 | 13 | @ToJson 14 | override fun toJson(writer: JsonWriter, value: ContactOrBot?) { 15 | writer.value(value?.id) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/util/GZIPHelper.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.util 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.ByteArrayOutputStream 5 | import java.nio.charset.StandardCharsets 6 | import java.util.* 7 | import java.util.zip.GZIPInputStream 8 | import java.util.zip.GZIPOutputStream 9 | 10 | fun String.toGZIPByteArray(): ByteArray { 11 | val output = ByteArrayOutputStream() 12 | val gzip = GZIPOutputStream(output) 13 | gzip.write(toByteArray(StandardCharsets.UTF_8)) 14 | gzip.close() 15 | return output.toByteArray() 16 | } 17 | 18 | fun String.toGZIPCompressedBase64Encoded(): String { 19 | return Base64.getEncoder().encodeToString(toGZIPByteArray()) 20 | } 21 | 22 | fun String.decodeGZIPBase64(): String { 23 | val input = GZIPInputStream(ByteArrayInputStream(Base64.getDecoder().decode(toByteArray(StandardCharsets.UTF_8)))) 24 | val data = input.readAllBytes() 25 | input.close() 26 | return String(data, StandardCharsets.UTF_8) 27 | } 28 | -------------------------------------------------------------------------------- /destinybot-core/src/main/kotlin/net/origind/destinybot/core/util/MemberData.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.core.util 2 | 3 | import com.squareup.moshi.JsonClass 4 | import net.mamoe.mirai.contact.NormalMember 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class MemberData(val id: Long, val permission: Int, val nick: String, val name: String, val rank: String, val joinTimestamp: Int, val lastSpeakTimestamp: Int) { 8 | constructor(member: NormalMember) : this(member.id, member.permission.ordinal, member.nameCard, member.nick, member.specialTitle, member.joinTimestamp, member.lastSpeakTimestamp) 9 | } 10 | -------------------------------------------------------------------------------- /destinybot-core/src/main/resources/lang/lang.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectUranus/destiny-bot-mirai/d06fdc4e35a3991a14a9ee69b9f8ddd5ba5def3e/destinybot-core/src/main/resources/lang/lang.properties -------------------------------------------------------------------------------- /destinybot-core/src/main/resources/lang/lang_en_US.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectUranus/destiny-bot-mirai/d06fdc4e35a3991a14a9ee69b9f8ddd5ba5def3e/destinybot-core/src/main/resources/lang/lang_en_US.properties -------------------------------------------------------------------------------- /destinybot-core/src/main/resources/lang/lang_zh_CN.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectUranus/destiny-bot-mirai/d06fdc4e35a3991a14a9ee69b9f8ddd5ba5def3e/destinybot-core/src/main/resources/lang/lang_zh_CN.properties -------------------------------------------------------------------------------- /destinybot-core/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | destinybot 8 | 9 | 10 | 11 | [%d{HH:mm:ss}] [%t/%level]: %msg%n 12 | UTF-8 13 | 14 | 15 | 16 | 17 | 18 | INFO 19 | 20 | 21 | ${LOG_HOME}/log.%d{yyyy-MM-dd}.log 22 | 10 23 | 24 | 25 | UTF-8 26 | [%d{HH:mm:ss}] [%t/%level]: %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | error 34 | 35 | 36 | ${LOG_HOME}/error.%d{yyyy-MM-dd}.log 37 | 10 38 | 39 | 40 | UTF-8 41 | [%d{HH:mm:ss}] [%t/%level]: %msg%n 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /destinybot-core/src/main/resources/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoSubmit": [ "托管", "启用托管" ], 3 | "disableAutoSubmit": [ "关闭托管", "结束托管", "取消托管" ], 4 | "draw": [ 5 | "摸", 6 | "摸牌", 7 | "mo", 8 | "draw", 9 | "画画", 10 | "抽卡!", 11 | "抽卡", 12 | "摸了", 13 | "cnm", 14 | "膜", 15 | "膜了", 16 | "苟了" 17 | ], 18 | "lastCard": [ "上一张牌", "全场信息" ], 19 | "myRound": [ "我的回合", "我的回合!", "我的回合!" ], 20 | "publicCard": [ "明牌" ], 21 | "doubt": [ 22 | "质疑", 23 | "喵喵喵?", 24 | "喵喵喵", 25 | "喵喵喵?" 26 | ], 27 | "nonDoubt": [ 28 | "不质疑", 29 | "nope", 30 | "nop", 31 | "Nop", 32 | "不" 33 | ], 34 | "uno": [ "UNO", "UNO!", "UNO!", "uno", "uNo", "uNO", "uno!", "uno!" ], 35 | "doubtUno": [ "质疑uno", "质疑UNO", "UNO PLZ", "uno plz" ], 36 | "join": [ "加入uno", "加入UNO" ], 37 | "leave": [ "离开uno", "离开UNO" ], 38 | "start": [ "开始UNO", "开始uno" ], 39 | "addRobot": [ "添加机器人" ] 40 | } -------------------------------------------------------------------------------- /destinybot-features/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.kotlin.jvm' 4 | } 5 | 6 | group 'net.origind.destinybot.features' 7 | version project["features.version"] 8 | 9 | sourceCompatibility = targetCompatibility = "16" 10 | 11 | repositories { 12 | mavenCentral() 13 | maven { url 'https://jitpack.io' } 14 | maven { url 'https://repo.opencollab.dev/maven-releases' } 15 | } 16 | 17 | dependencies { 18 | implementation project(":destinybot-api") 19 | 20 | implementation 'com.electronwill.night-config:core:3.6.4' 21 | 22 | implementation "io.ktor:ktor-client-core-jvm:$ktorVersion" 23 | implementation "io.ktor:ktor-client-okhttp:$ktorVersion" 24 | implementation group: 'com.squareup.moshi', name: 'moshi', version: '1.9.2' 25 | implementation group: 'com.squareup.moshi', name: 'moshi-kotlin', version: '1.9.2' 26 | implementation 'org.litote.kmongo:kmongo:4.8.0' 27 | implementation 'org.litote.kmongo:kmongo-coroutine:4.8.0' 28 | implementation 'com.squareup.okhttp3:okhttp:4.10.0' 29 | 30 | implementation 'it.unimi.dsi:fastutil:8.5.6' 31 | implementation 'com.projecturanus:suffixtree:1.0' 32 | 33 | implementation 'com.github.steveice10:mcprotocollib:1.19.2-1' 34 | 35 | 36 | testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' 37 | testRuntimeOnly 'ch.qos.logback:logback-classic:1.3.0-beta0' 38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 39 | } 40 | 41 | test { 42 | useJUnitPlatform() 43 | } 44 | 45 | compileKotlin { 46 | kotlinOptions.jvmTarget = "16" 47 | } 48 | compileTestKotlin { 49 | kotlinOptions.jvmTarget = "16" 50 | } 51 | -------------------------------------------------------------------------------- /destinybot-features/chessboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectUranus/destiny-bot-mirai/d06fdc4e35a3991a14a9ee69b9f8ddd5ba5def3e/destinybot-features/chessboard.png -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/module-info.java: -------------------------------------------------------------------------------- 1 | import net.origind.destinybot.api.plugin.Plugin; 2 | import net.origind.destinybot.features.FeaturesPlugin; 3 | 4 | module destinybot.features { 5 | requires org.mongodb.driver.sync.client; 6 | requires com.squareup.moshi.kotlin; 7 | requires com.squareup.moshi; 8 | requires kmongo.core; 9 | requires org.mongodb.bson; 10 | 11 | requires transitive destinybot.api; 12 | requires kotlin.stdlib; 13 | requires okhttp3; 14 | requires kotlinx.coroutines.core.jvm; 15 | requires mcprotocollib; 16 | requires packetlib; 17 | requires io.ktor.client.core; 18 | requires java.desktop; 19 | requires org.slf4j; 20 | requires com.electronwill.nightconfig.core; 21 | requires suffixtree; 22 | requires it.unimi.dsi.fastutil; 23 | requires net.kyori.adventure; 24 | 25 | exports net.origind.destinybot.features; 26 | exports net.origind.destinybot.features.apex; 27 | exports net.origind.destinybot.features.bilibili; 28 | exports net.origind.destinybot.features.destiny; 29 | exports net.origind.destinybot.features.github; 30 | exports net.origind.destinybot.features.instatus; 31 | exports net.origind.destinybot.features.minecraft; 32 | exports net.origind.destinybot.features.yahtzee; 33 | exports net.origind.destinybot.features.romajitable; 34 | 35 | provides Plugin with FeaturesPlugin; 36 | } 37 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/DataStore.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.coroutineScope 7 | import kotlinx.coroutines.launch 8 | import net.origind.destinybot.features.destiny.data.UserData 9 | import org.slf4j.Logger 10 | import org.slf4j.LoggerFactory 11 | import java.nio.charset.StandardCharsets 12 | import java.nio.file.Files 13 | import java.nio.file.Path 14 | import java.nio.file.Paths 15 | import java.nio.file.StandardOpenOption 16 | 17 | object DataStore { 18 | private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() 19 | private val users = hashMapOf() 20 | private val writeOptions = arrayOf(StandardOpenOption.WRITE, StandardOpenOption.CREATE) 21 | 22 | val logger: Logger = LoggerFactory.getLogger("DataStore") 23 | private val usersDirectory: Path = Paths.get("users").toAbsolutePath() 24 | 25 | operator fun get(id: Long) = users.getOrPut(id) { UserData(id) } 26 | 27 | operator fun contains(id: Long) = users.containsKey(id) 28 | 29 | suspend fun init() { 30 | if (Files.notExists(usersDirectory)) { 31 | coroutineScope { 32 | launch(Dispatchers.IO) { 33 | Files.createDirectories(usersDirectory) 34 | logger.info("Created users directory {}", usersDirectory.toString()) 35 | } 36 | } 37 | } 38 | load() 39 | } 40 | 41 | suspend fun save() { 42 | coroutineScope { 43 | for ((id, user) in users) { 44 | launch(Dispatchers.IO) { 45 | Files.writeString( 46 | usersDirectory.resolve("$id.json"), 47 | moshi.adapter(UserData::class.java).toJson(user), 48 | *writeOptions 49 | ) 50 | } 51 | } 52 | } 53 | logger.info("Saved {} users", users.size) 54 | } 55 | 56 | suspend fun load() { 57 | users.clear() 58 | coroutineScope { 59 | launch(Dispatchers.IO) { 60 | Files.list(usersDirectory).forEach { 61 | val user = 62 | moshi.adapter(UserData::class.java).fromJson(Files.readString(it, StandardCharsets.UTF_8)) 63 | if (user != null) 64 | users[user.qq] = user 65 | else 66 | logger.warn("Cannot read user file $it") 67 | } 68 | logger.info("Loaded {} users", users.size) 69 | } 70 | } 71 | } 72 | 73 | suspend fun reload() { 74 | save() 75 | load() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/Database.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features 2 | 3 | import com.mongodb.client.MongoClient 4 | import com.mongodb.client.MongoDatabase 5 | import com.squareup.moshi.Moshi 6 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 7 | import net.origind.destinybot.features.destiny.response.DestinyActivityDefinition 8 | import net.origind.destinybot.features.destiny.response.lightgg.DisplayProperties 9 | import net.origind.destinybot.features.destiny.response.lightgg.ItemDefinition 10 | import org.litote.kmongo.KMongo 11 | import org.litote.kmongo.find 12 | import org.litote.kmongo.findOne 13 | 14 | object Database { 15 | val mongoClient: MongoClient 16 | val db: MongoDatabase 17 | 18 | private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() 19 | 20 | init { 21 | mongoClient = KMongo.createClient() 22 | db = mongoClient.getDatabase("destiny2") 23 | } 24 | 25 | fun getItemDefinition(itemId: String): ItemDefinition { 26 | return moshi.adapter(ItemDefinition::class.java).fromJson(db.getCollection("DestinyInventoryItemDefinition_chs").findOne("""{"hash": $itemId}""")?.toJson()!!)!! 27 | } 28 | 29 | fun getItemDefinitions(displayName: String): List { 30 | val itemDefinitionCollection = db.getCollection("DestinyInventoryItemDefinition_chs") 31 | return itemDefinitionCollection.find("""{"displayProperties.name": "$displayName"}""").map { document -> 32 | moshi.adapter(ItemDefinition::class.java).fromJson(document.toJson())!! 33 | }.toList() 34 | } 35 | 36 | fun translate(name: String): DisplayProperties? { 37 | val itemDefinitionCollection = db.getCollection("DestinyInventoryItemDefinition_eng") 38 | return itemDefinitionCollection.find("""{"displayProperties.name": "$name"}""").map { document -> 39 | getItemDefinition(document["_id"].toString()).displayProperties 40 | }.firstOrNull() 41 | } 42 | 43 | fun getActivity(id: Long): DestinyActivityDefinition { 44 | return moshi.adapter(DestinyActivityDefinition::class.java).fromJson(db.getCollection("DestinyActivityDefinition_chs_chs").findOne("""{"hash": $id}""")?.toJson()!!)!! 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/FeaturesPlugin.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 6 | import net.origind.destinybot.api.command.CommandManager 7 | import net.origind.destinybot.api.plugin.Plugin 8 | import net.origind.destinybot.features.apex.MapRotationCommand 9 | import net.origind.destinybot.features.apex.ProfileCommand 10 | import net.origind.destinybot.features.bilibili.BilibiliConfig 11 | import net.origind.destinybot.features.bilibili.StreamerCommand 12 | import net.origind.destinybot.features.bilibili.VTuberCommand 13 | import net.origind.destinybot.features.bilibili.bilibiliConfig 14 | import net.origind.destinybot.features.destiny.* 15 | import net.origind.destinybot.features.github.GitHubCommand 16 | import net.origind.destinybot.features.injdk.InjdkCommand 17 | import net.origind.destinybot.features.instatus.InstatusAPI 18 | import net.origind.destinybot.features.minecraft.MinecraftConfig 19 | import net.origind.destinybot.features.minecraft.MinecraftVersionCommand 20 | import net.origind.destinybot.features.minecraft.PingCommand 21 | import net.origind.destinybot.features.minecraft.minecraftConfig 22 | import net.origind.destinybot.features.timer.TimerCommand 23 | import org.slf4j.Logger 24 | import org.slf4j.LoggerFactory 25 | 26 | val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() 27 | val logger: Logger = LoggerFactory.getLogger("DestinyBot Features") 28 | 29 | class FeaturesPlugin : Plugin { 30 | override val name: String = "features" 31 | override val version: String = "1.0.0" 32 | 33 | override fun init() { 34 | } 35 | 36 | override fun reloadConfig(config: Config) { 37 | bilibiliConfig = BilibiliConfig(config) 38 | minecraftConfig = MinecraftConfig(config) 39 | PingCommand.reloadConfig(config) 40 | InstatusAPI.reloadConfig(config) 41 | } 42 | 43 | override suspend fun reload() { 44 | MinecraftVersionCommand.reload() 45 | InjdkCommand.reload() 46 | } 47 | 48 | override fun registerCommand(manager: CommandManager) { 49 | // Apex Commands 50 | manager.register(MapRotationCommand) 51 | manager.register(ProfileCommand) 52 | 53 | // Bilibili Commands 54 | manager.register(StreamerCommand) 55 | manager.register(VTuberCommand) 56 | 57 | // Destiny Commands 58 | manager.register(ActivityCommand) 59 | manager.register(BindAccountCommand) 60 | manager.register(LoreCommand) 61 | manager.register(MyProfileCommand) 62 | manager.register(PerkCommand) 63 | manager.register(PlayerProfileCommand) 64 | manager.register(QueryLinkedCredentialCommand) 65 | manager.register(SearchChooseResultCommand) 66 | manager.register(SearchCommand) 67 | manager.register(TrackerCommand) 68 | manager.register(WeeklyReportCommand) 69 | 70 | manager.register(InjdkCommand) 71 | 72 | // GitHub Commands 73 | manager.register(GitHubCommand) 74 | 75 | // Minecraft Commands 76 | manager.register(PingCommand) 77 | manager.register(MinecraftVersionCommand) 78 | manager.register(TimerCommand) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/NetworkHelper.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.coroutineScope 6 | import okhttp3.Cache 7 | import okhttp3.OkHttpClient 8 | import okhttp3.Request 9 | import java.io.File 10 | import java.util.concurrent.TimeUnit 11 | 12 | val client: OkHttpClient = OkHttpClient.Builder() 13 | .cache(Cache(directory = File("web_cache"), maxSize = 10L * 1024L * 1024L)) 14 | .connectTimeout(10, TimeUnit.SECONDS) 15 | .readTimeout(10, TimeUnit.SECONDS) 16 | .retryOnConnectionFailure(true) 17 | .followRedirects(true) 18 | .followSslRedirects(true) 19 | .callTimeout(15, TimeUnit.SECONDS) 20 | .build() 21 | 22 | 23 | suspend inline fun getBodyAsync(url: String, crossinline init: Request.Builder.() -> Unit = {}) = coroutineScope { 24 | val request = Request.Builder().apply { 25 | url(url) 26 | init() 27 | }.build() 28 | val call = client.newCall(request) 29 | val response = async(Dispatchers.IO) { call.execute().body?.string() ?: "" } 30 | response 31 | } 32 | 33 | suspend inline fun getJson(url: String, crossinline init: Request.Builder.() -> Unit = {}): T = 34 | moshi.adapter(T::class.java).fromJson(getBodyAsync(url, init).await())!! 35 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/apex/ApexAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.apex 2 | 3 | import net.origind.destinybot.features.apex.response.ApexMapRotation 4 | import net.origind.destinybot.features.apex.response.ApexPlayer 5 | import net.origind.destinybot.features.getJson 6 | import java.io.IOException 7 | 8 | const val APEX_API_ENDPOINT = "https://api.mozambiquehe.re" 9 | const val APEX_API_KEY = "7wMiKAiijuIC1Ts99Ek8" 10 | 11 | class ApexApiException(message: String) : IOException(message) 12 | 13 | fun localizeMapName(map: String) = when(map) { 14 | "Kings Canyon" -> "诸王峡谷" 15 | "World's Edge" -> "世界尽头" 16 | "Olympus" -> "奥林匹斯" 17 | "Storm Point" -> "风暴点" 18 | else -> map 19 | } 20 | 21 | fun localizeRankName(rank: String) = when(rank) { 22 | "Bronze" -> "青铜" 23 | "Silver" -> "白银" 24 | "Gold" -> "黄金" 25 | "Platinum" -> "铂金" 26 | "Diamond" -> "钻石" 27 | "Master" -> "大师" 28 | "Apex Predator" -> "猎杀" 29 | else -> "无" 30 | } 31 | 32 | suspend fun searchApexPlayer(name: String): ApexPlayer 33 | = getJson("$APEX_API_ENDPOINT/bridge?version=5&platform=PC&player=${name}&auth=$APEX_API_KEY") 34 | 35 | suspend fun getMapRotation() : ApexMapRotation = getJson("$APEX_API_ENDPOINT/maprotation?version=2&auth=$APEX_API_KEY") 36 | 37 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/apex/MapRotationCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.apex 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | import net.origind.destinybot.api.util.toLocalizedString 8 | import java.time.Duration 9 | 10 | object MapRotationCommand: AbstractCommand("地图轮换") { 11 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 12 | try { 13 | val rotation = getMapRotation() 14 | executor.sendMessage(buildString { 15 | appendLine("当前大逃杀模式地图:${localizeMapName(rotation.battleRoyale.current.map)},将在 ${Duration.ofSeconds(rotation.battleRoyale.current.remainingSecs!!).toLocalizedString()} 后切换为 ${localizeMapName(rotation.battleRoyale.next.map)}。") 16 | append("排名赛地图:${localizeMapName(rotation.ranked.current.map)},下一张地图为 ${localizeMapName(rotation.ranked.next.map)}") 17 | }) 18 | } catch (e: Exception) { 19 | executor.sendMessage("请求时发生了错误;${e.localizedMessage},请稍后重试。") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/apex/ProfileCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.apex 2 | 3 | import net.origind.destinybot.api.command.* 4 | 5 | object ProfileCommand: AbstractCommand("apex开盒") { 6 | init { 7 | arguments += ArgumentContext("player", StringArgument) 8 | } 9 | 10 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 11 | val name = argument.getArgument("player") 12 | try { 13 | val player = searchApexPlayer(name) 14 | if (player.Error != null) { 15 | executor.sendMessage("查询不到 $name,当前只能查询 Origin 平台上的名称") 16 | return 17 | } 18 | executor.sendMessage(buildString { 19 | appendLine("玩家:${player.global.name}") 20 | if (player.realtime.currentState != "offline") { 21 | append(",当前在线") 22 | } 23 | appendLine("ID:${player.global.uid}") 24 | appendLine("等级:${player.global.level},升级进度为 ${player.global.toNextLevelPercent}%") 25 | appendLine("段位:${localizeRankName(player.global.rank.rankName)} ${player.global.rank.rankDiv}") 26 | if (player.global.battlepass.level != "-1") { 27 | appendLine("通行证等级:${player.global.battlepass.level}") 28 | } 29 | append("总击杀:${player.total.kills?.value ?: 0}") 30 | }) 31 | } catch (e: Exception) { 32 | executor.sendMessage("请求时发生了错误;${e.localizedMessage},请稍后重试。") 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/apex/response/ApexMapRotation.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.apex.response 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class ApexMapRotation ( 8 | @Json(name = "battle_royale") 9 | val battleRoyale: Rotation, 10 | 11 | val arenas: Rotation, 12 | val ranked: RankedRotation, 13 | val arenasRanked: Rotation 14 | ) 15 | 16 | @JsonClass(generateAdapter = true) 17 | data class Rotation ( 18 | val current: RotationData, 19 | val next: RotationData 20 | ) 21 | 22 | @JsonClass(generateAdapter = true) 23 | data class RotationData ( 24 | val start: Long, 25 | val end: Long, 26 | 27 | @Json(name = "readableDate_start") 28 | val readableDateStart: String, 29 | 30 | @Json(name = "readableDate_end") 31 | val readableDateEnd: String, 32 | 33 | val map: String, 34 | 35 | @Json(name = "DurationInSecs") 36 | val durationInSecs: Long, 37 | 38 | @Json(name = "DurationInMinutes") 39 | val durationInMinutes: Long, 40 | 41 | // Ranked will have these values null 42 | val remainingSecs: Long?, 43 | val remainingMins: Long?, 44 | val remainingTimer: String? 45 | ) 46 | 47 | @JsonClass(generateAdapter = true) 48 | data class RankedRotation ( 49 | val current: BasicRotationData, 50 | val next: BasicRotationData 51 | ) 52 | 53 | @JsonClass(generateAdapter = true) 54 | data class BasicRotationData ( 55 | val map: String 56 | ) 57 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/apex/response/ApexPlayer.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.apex.response 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class ApexPlayer ( 8 | val global: GlobalData, 9 | val realtime: RealtimeData, 10 | // val legends: LegendsData, 11 | 12 | val total: TotalData, 13 | val Error: String? 14 | ) 15 | 16 | @JsonClass(generateAdapter = true) 17 | data class GlobalData ( 18 | val name: String, 19 | val uid: Long, 20 | val avatar: String, 21 | val platform: String, 22 | val level: Long, 23 | val toNextLevelPercent: Long, 24 | val internalUpdateCount: Long, 25 | val bans: Bans, 26 | val rank: Arena, 27 | val arena: Arena, 28 | val battlepass: Battlepass, 29 | val badges: List? 30 | ) 31 | 32 | @JsonClass(generateAdapter = true) 33 | data class Arena ( 34 | val rankScore: Long, 35 | val rankName: String, 36 | val rankDiv: Long, 37 | val ladderPosPlatform: Long, 38 | val rankImg: String, 39 | val rankedSeason: String 40 | ) 41 | 42 | @JsonClass(generateAdapter = true) 43 | data class Bans ( 44 | val isActive: Boolean, 45 | val remainingSeconds: Long, 46 | 47 | @Json(name = "last_banReason") 48 | val lastBanReason: String 49 | ) 50 | 51 | @JsonClass(generateAdapter = true) 52 | data class Battlepass ( 53 | val level: String, 54 | val history: Map 55 | ) 56 | 57 | @JsonClass(generateAdapter = true) 58 | data class LegendsData ( 59 | val selected: Selected, 60 | val all: All 61 | ) 62 | 63 | @JsonClass(generateAdapter = true) 64 | data class All ( 65 | @Json(name = "Revenant") 66 | val revenant: LegendStats, 67 | 68 | @Json(name = "Crypto") 69 | val crypto: LegendStats, 70 | 71 | @Json(name = "Horizon") 72 | val horizon: LegendStats, 73 | 74 | @Json(name = "Gibraltar") 75 | val gibraltar: LegendStats, 76 | 77 | @Json(name = "Wattson") 78 | val wattson: LegendStats, 79 | 80 | @Json(name = "Fuse") 81 | val fuse: LegendStats, 82 | 83 | @Json(name = "Bangalore") 84 | val bangalore: LegendStats, 85 | 86 | @Json(name = "Wraith") 87 | val wraith: LegendStats, 88 | 89 | @Json(name = "Octane") 90 | val octane: LegendStats, 91 | 92 | @Json(name = "Bloodhound") 93 | val bloodhound: LegendStats, 94 | 95 | @Json(name = "Caustic") 96 | val caustic: LegendStats, 97 | 98 | @Json(name = "Lifeline") 99 | val lifeline: LegendStats, 100 | 101 | @Json(name = "Pathfinder") 102 | val pathfinder: LegendStats, 103 | 104 | @Json(name = "Loba") 105 | val loba: LegendStats, 106 | 107 | @Json(name = "Mirage") 108 | val mirage: LegendStats, 109 | 110 | @Json(name = "Rampart") 111 | val rampart: LegendStats, 112 | 113 | @Json(name = "Valkyrie") 114 | val valkyrie: LegendStats, 115 | 116 | @Json(name = "Seer") 117 | val seer: LegendStats 118 | ) 119 | 120 | @JsonClass(generateAdapter = true) 121 | data class LegendStats ( 122 | val data: List?, 123 | val gameInfo: LegendGameInfo?, 124 | 125 | @Json(name = "ImgAssets") 126 | val imgAssets: ImgAssets 127 | ) 128 | 129 | @JsonClass(generateAdapter = true) 130 | data class LegendStat ( 131 | val name: String, 132 | val value: Long, 133 | val key: String, 134 | val rank: Rank, 135 | val rankPlatformSpecific: Rank 136 | ) 137 | 138 | @JsonClass(generateAdapter = true) 139 | data class Rank ( 140 | val rankPos: Long, 141 | val topPercent: Double 142 | ) 143 | 144 | @JsonClass(generateAdapter = true) 145 | data class LegendGameInfo ( 146 | val badges: List 147 | ) 148 | 149 | @JsonClass(generateAdapter = true) 150 | data class ImgAssets ( 151 | val icon: String, 152 | val banner: String 153 | ) 154 | 155 | 156 | @JsonClass(generateAdapter = true) 157 | data class Selected ( 158 | @Json(name = "LegendName") 159 | val legendName: String, 160 | 161 | val data: List, 162 | val gameInfo: SelectedGameInfo, 163 | 164 | @Json(name = "ImgAssets") 165 | val imgAssets: ImgAssets 166 | ) 167 | 168 | @JsonClass(generateAdapter = true) 169 | data class SelectedDatum ( 170 | val name: String, 171 | val value: Long, 172 | val key: String 173 | ) 174 | 175 | @JsonClass(generateAdapter = true) 176 | data class SelectedGameInfo ( 177 | val skin: String, 178 | val skinRarity: String, 179 | val frame: String, 180 | val frameRarity: String, 181 | val pose: String, 182 | val poseRarity: String, 183 | val intro: String, 184 | val introRarity: String, 185 | val badges: List 186 | ) 187 | 188 | @JsonClass(generateAdapter = true) 189 | data class Badge ( 190 | val name: String?, 191 | val value: Long?, 192 | val category: String? 193 | ) 194 | 195 | @JsonClass(generateAdapter = true) 196 | data class RealtimeData ( 197 | val lobbyState: String, 198 | val isOnline: Long, 199 | val isInGame: Long, 200 | val canJoin: Long, 201 | val partyFull: Long, 202 | val selectedLegend: String, 203 | val currentState: String, 204 | val currentStateSinceTimestamp: Long, 205 | val currentStateAsText: String 206 | ) 207 | 208 | @JsonClass(generateAdapter = true) 209 | data class TotalData ( 210 | val kills: Badge?, 211 | 212 | @Json(name = "wins_season_3") 213 | val winsSeason3: Badge?, 214 | 215 | @Json(name = "wins_season_4") 216 | val winsSeason4: Badge?, 217 | 218 | @Json(name = "games_played") 219 | val gamesPlayed: Badge?, 220 | 221 | @Json(name = "wins_season_1") 222 | val winsSeason1: Badge?, 223 | 224 | @Json(name = "creeping_barrage_damage") 225 | val creepingBarrageDamage: Badge?, 226 | 227 | @Json(name = "kills_season_1") 228 | val killsSeason1: Badge?, 229 | 230 | @Json(name = "wins_season_2") 231 | val winsSeason2: Badge?, 232 | 233 | @Json(name = "top_3") 234 | val top3: Badge?, 235 | 236 | @Json(name = "beast_of_the_hunt_kills") 237 | val beastOfTheHuntKills: Badge?, 238 | 239 | val damage: Badge?, 240 | 241 | @Json(name = "dropped_items_for_squadmates") 242 | val droppedItemsForSquadmates: Badge?, 243 | 244 | @Json(name = "pistol_kills") 245 | val pistolKills: Badge?, 246 | 247 | @Json(name = "beacons_scanned") 248 | val beaconsScanned: Badge?, 249 | 250 | @Json(name = "ar_kills") 251 | val Badge: Badge?, 252 | 253 | val kd: Kd? 254 | ) 255 | 256 | @JsonClass(generateAdapter = true) 257 | data class Kd ( 258 | val value: String, 259 | val name: String 260 | ) 261 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/BilibiliAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import net.origind.destinybot.features.getBodyAsync 4 | import net.origind.destinybot.features.getJson 5 | import okhttp3.FormBody 6 | 7 | suspend fun getArticleListIDs(): List { 8 | val articles = getJson("https://api.bilibili.com/x/article/list/articles?id=175327&jsonp=jsonp").data.articles 9 | return articles.map { it.id } 10 | } 11 | 12 | suspend fun getLatestArticle(): String { 13 | return getBodyAsync("https://www.bilibili.com/read/cv${getArticleListIDs().last()}/?from=readlist").await() 14 | } 15 | 16 | suspend fun getLatestWeeklyReportURL(): String { 17 | val regex = Regex("("https://api.live.bilibili.com/room/v1/Room/get_info?id=$id").data 24 | } 25 | 26 | suspend fun searchUser(keyword: String): List = 27 | getJson("http://api.bilibili.com/x/web-interface/search/type?search_type=bili_user&keyword=$keyword&page=1") 28 | .data?.result ?: emptyList() 29 | 30 | suspend fun follow(csrf: String, cookie: String, fid: String): BilibiliResponse = 31 | getJson("https://api.bilibili.com/x/relation/modify") { 32 | header("Cookie", cookie) 33 | post(FormBody.Builder() 34 | .addEncoded("fid", fid) 35 | .addEncoded("act", "1") 36 | .addEncoded("re_src", "11") 37 | .addEncoded("csrf", csrf) 38 | .build()) 39 | } 40 | 41 | suspend fun sameFollow(cookie: String, vmid: Long) = 42 | getJson("https://api.bilibili.com/x/relation/same/followings?ps=3000&vmid=$vmid") { 43 | header("Cookie", cookie) 44 | }.data?.list ?: emptyList() 45 | 46 | suspend fun getUserInfo(mid: Long) = 47 | getJson("https://api.bilibili.com/x/space/acc/info?mid=$mid"){ 48 | header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0") 49 | }.data 50 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/BilibiliConfig.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | 5 | class BilibiliConfig(config: Config) { 6 | val lives: MutableList = config.getOrElse("bilibili.lives", mutableListOf()) 7 | val cookie: String = config.getOrElse("bilibili.cookie", "") 8 | val replyStreamersTo: MutableList = config.getOrElse("bilibili.reply_to", mutableListOf()) 9 | } 10 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/BilibiliResponses.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class Article(val id: Int, val publish_time: Int, val title: String) 7 | 8 | @JsonClass(generateAdapter = true) 9 | data class ArticlesData(val articles: List
) 10 | 11 | @JsonClass(generateAdapter = true) 12 | data class Articles(val data: ArticlesData, val ttl: Int) 13 | 14 | @JsonClass(generateAdapter = true) 15 | data class LiveRoomInfo(val uid: Long, val room_id: Int, val title: String, val online: Int, val live_status: Int) { 16 | override fun equals(other: Any?): Boolean { 17 | if (this === other) return true 18 | if (other !is LiveRoomInfo) return false 19 | 20 | if (uid != other.uid) return false 21 | if (room_id != other.room_id) return false 22 | 23 | return true 24 | } 25 | 26 | override fun hashCode(): Int { 27 | var result = uid.hashCode() 28 | result = 31 * result + room_id 29 | return result 30 | } 31 | } 32 | 33 | @JsonClass(generateAdapter = true) 34 | data class LiveResponse(val code: Int, val data: LiveRoomInfo) 35 | 36 | @JsonClass(generateAdapter = true) 37 | data class BilibiliResponse(var code: Int = 0, var message: String = "", var ttl: Int = 0) 38 | 39 | @JsonClass(generateAdapter = true) 40 | data class BilibiliDataResponse(var code: Int = 0, var message: String = "", var ttl: Int = 0, var data: T) 41 | 42 | @JsonClass(generateAdapter = true) 43 | data class BilibiliUserInfoResponse(var code: Int = 0, var message: String = "", var ttl: Int = 0, var data: BilibiliUser? = null) 44 | 45 | @JsonClass(generateAdapter = true) 46 | data class BilibiliQueryResponse(var numResults: Int = 0, var numPages: Int = 0, var page: Int = 0, var result: List = emptyList()) 47 | 48 | @JsonClass(generateAdapter = true) 49 | data class BilibiliUserDataResponse(var code: Int = 0, var message: String = "", var ttl: Int = 0, var data: BilibiliUserQueryResponse? = null) 50 | 51 | @JsonClass(generateAdapter = true) 52 | data class BilibiliUserQueryResponse(var numResults: Int = 0, var numPages: Int = 0, var page: Int = 0, var result: List = emptyList()) 53 | 54 | @JsonClass(generateAdapter = true) 55 | data class BilibiliSameFollowResponse(var code: Int = 0, var message: String = "", var ttl: Int = 0, var data: BilibiliSameFollowDataResponse? = null) 56 | 57 | @JsonClass(generateAdapter = true) 58 | data class BilibiliSameFollowDataResponse(var total: Int = 0, var list: List = emptyList()) 59 | 60 | @JsonClass(generateAdapter = true) 61 | data class BilibiliUser(var mid: Long? = null, var name: String? = null) { 62 | fun name(): String = name ?: mid?.toString() ?: "null" 63 | } 64 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/ManageStreamerCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import net.origind.destinybot.api.command.* 4 | 5 | class ManageStreamerCommand: AbstractCommand("添加主播") { 6 | 7 | init { 8 | permission = "admin.config.set" 9 | arguments += ArgumentContext("value", LongArgument) 10 | } 11 | 12 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 13 | val value = argument.getArgument("value") 14 | 15 | bilibiliConfig.lives.add(value) 16 | println("已添加") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/StreamerCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.awaitAll 6 | import kotlinx.coroutines.coroutineScope 7 | import net.origind.destinybot.api.command.AbstractCommand 8 | import net.origind.destinybot.api.command.ArgumentContainer 9 | import net.origind.destinybot.api.command.CommandContext 10 | import net.origind.destinybot.api.command.CommandExecutor 11 | 12 | lateinit var bilibiliConfig: BilibiliConfig 13 | 14 | object StreamerCommand: AbstractCommand("下饭主播") { 15 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 16 | executor.sendMessage(coroutineScope { 17 | buildString { 18 | var anyOnline = false 19 | bilibiliConfig.lives 20 | .map { async(Dispatchers.IO) { getLiveRoomInfo(it) } } 21 | .awaitAll() 22 | .asSequence() 23 | .filter { it.live_status == 1 } 24 | .forEach { roomInfo -> 25 | appendLine("你喜爱的主播 " + getUserInfo(roomInfo.uid)?.name() + " 正在直播: ${roomInfo.title}!https://live.bilibili.com/${roomInfo.room_id}") 26 | anyOnline = true 27 | } 28 | if (!anyOnline) append("你喜爱的主播们都不在直播哦O(∩_∩)O") 29 | }.trim() 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/VTuberCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import net.origind.destinybot.api.command.* 4 | 5 | object VTuberCommand : AbstractCommand("查成分") { 6 | init { 7 | arguments += ArgumentContext("name", StringArgument) 8 | } 9 | 10 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 11 | val name = argument.getArgument("name").trim() 12 | val id = name.toLongOrNull() 13 | val info = if (id != null) { 14 | val i = searchUser(name).firstOrNull() 15 | if (i?.mid != 0L) 16 | i 17 | else 18 | getUserInfo(id) 19 | } else { 20 | searchUser(name).firstOrNull() 21 | } ?: BilibiliUser(0, "") 22 | 23 | if (info.mid == 0L) { 24 | executor.sendMessage("获取用户信息失败,请检查你的搜索关键词或mid。") 25 | } else { 26 | executor.sendMessage("${info.name()} 关注的 VUP 有:" + sameFollow(bilibiliConfig.cookie, info?.mid ?: 0).joinToString { it.name() }) 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/VdbAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili 2 | 3 | import net.origind.destinybot.features.bilibili.vdb.VTuberList 4 | import net.origind.destinybot.features.getJson 5 | 6 | object VdbAPI { 7 | const val ENDPOINT = "https://api.vtbs.moe" 8 | const val LIST = "https://vdb.vtbs.moe/json/list.json" 9 | 10 | var vTuberList = VTuberList() 11 | 12 | suspend fun updateList() { 13 | vTuberList = getJson(LIST) 14 | } 15 | } -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/bilibili/vdb/VdbResponses.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.bilibili.vdb 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class VTuberList(var vtbs: List = emptyList()) 7 | 8 | @JsonClass(generateAdapter = true) 9 | data class VTuberMeta(var uuid: String = "", var type: String = "", var bot: Boolean = false, 10 | var accounts: List = emptyList(), var name: VTuberName) 11 | 12 | @JsonClass(generateAdapter = true) 13 | data class VTuberAccount(var id: String, var type: String, var platform: String) 14 | 15 | @JsonClass(generateAdapter = true) 16 | data class VTuberName(var cn: String = "", var jp: String = "", var en: String = "", var default: String = "", var extra: List = emptyList()) -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/ActivityCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import kotlinx.coroutines.coroutineScope 4 | import kotlinx.coroutines.launch 5 | import net.origind.destinybot.api.command.AbstractCustomCommand 6 | import net.origind.destinybot.api.command.ArgumentContainer 7 | import net.origind.destinybot.api.command.CommandContext 8 | import net.origind.destinybot.api.command.CommandExecutor 9 | import net.origind.destinybot.features.Database 10 | import org.bson.Document 11 | import org.litote.kmongo.findOne 12 | 13 | object ActivityCommand: AbstractCustomCommand("查询活动") { 14 | val activities = hashMapOf() 15 | 16 | override suspend fun init() { 17 | coroutineScope { 18 | launch { 19 | val collection = Database.db.getCollection("DestinyActivityDefinition_chs") 20 | activities.putAll(collection.find().map { 21 | it.get("displayProperties", Document::class.java)?.getString("name")!! to it.getString("_id") 22 | }) 23 | } 24 | } 25 | } 26 | 27 | override suspend fun execute( 28 | main: String, 29 | argument: ArgumentContainer, 30 | executor: CommandExecutor, 31 | context: CommandContext 32 | ) { 33 | if (!main.startsWith("/")) return 34 | if (activities.containsKey(main.substring(1))) return 35 | 36 | val collection = Database.db.getCollection("DestinyActivityDefinition_chs") 37 | val doc = collection.findOne("""{"_id": "${activities[main.substring(1)]}"}""") 38 | executor.sendMessage(doc?.get("displayProperties", Document::class.java)?.getString("description") ?: "") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/BindAccountCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import io.ktor.client.plugins.* 4 | import net.origind.destinybot.api.command.* 5 | import net.origind.destinybot.features.DataStore 6 | 7 | object BindAccountCommand : AbstractCommand("绑定") { 8 | init { 9 | arguments += ArgumentContext("id", QQArgument) 10 | } 11 | 12 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 13 | val id = argument.getArgument("id") 14 | if (profileQuerys[context.senderId]?.get(id.toInt() - 1) == null) { 15 | 16 | // 直接绑定 ID 17 | if (id < 10000000) executor.sendMessage("你输入的命运2 ID是不是稍微短了点?") 18 | else { 19 | val destinyMembership = if (id.toString().startsWith("7656")) { 20 | getMembershipFromHardLinkedCredential(id.toString()) 21 | } else { 22 | getProfile(3, id.toString())?.profile?.data?.userInfo 23 | } 24 | 25 | if (destinyMembership == null) executor.sendMessage("无法找到该玩家,检查一下?") 26 | else { 27 | DataStore[context.senderId].apply { 28 | destinyMembershipId = destinyMembership.membershipId 29 | destinyMembershipType = destinyMembership.membershipType 30 | destinyDisplayName = destinyMembership.displayName 31 | } 32 | DataStore.save() 33 | executor.sendMessage("绑定 ${destinyMembership.displayName}(${destinyMembership.membershipId}) 到 ${context.senderId} 成功。") 34 | } 35 | } 36 | } else { 37 | // 绑定搜索序号 38 | val result = profileQuerys[context.senderId]!! 39 | val index = id - 1 40 | if (result.size < index + 1) executor.sendMessage("你的序号太大了点。") 41 | val destinyMembership = result[index.toInt()] 42 | try { 43 | DataStore[context.senderId].apply { 44 | destinyMembershipId = destinyMembership.membershipId 45 | destinyMembershipType = destinyMembership.membershipType 46 | destinyDisplayName = destinyMembership.displayName 47 | } 48 | DataStore.save() 49 | executor.sendMessage("绑定 ${destinyMembership.displayName}(${destinyMembership.membershipId}) 到 ${context.senderId} 成功。") 50 | } catch (e: ServerResponseException) { 51 | executor.sendMessage("获取详细信息时失败,请重试。\n${e.localizedMessage}") 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/BungieAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import io.ktor.client.network.sockets.* 4 | import kotlinx.coroutines.* 5 | import net.origind.destinybot.features.destiny.response.* 6 | import net.origind.destinybot.features.getJson 7 | import okhttp3.MediaType.Companion.toMediaType 8 | import okhttp3.RequestBody.Companion.toRequestBody 9 | 10 | const val endpoint = "https://www.bungie.net/Platform" 11 | const val key = "9654e41465f34fb6a7aea347abd5deeb" 12 | 13 | suspend fun getDestinyProfiles(displayName: String, displayNameCode: Int, membershipType: Int): DestinyMembershipQuery? = 14 | getJson("$endpoint/Destiny2/SearchDestinyPlayerByBungieName/$membershipType/") { 15 | header("X-API-Key", key) 16 | post("""{"displayName": "$displayName", "displayNameCode": $displayNameCode}""".toRequestBody("application/json".toMediaType())) 17 | }.Response.firstOrNull() 18 | 19 | suspend fun bungieUserToDestinyUser(displayName: String, displayNameCode: Int): DestinyMembershipQuery? = withContext(Dispatchers.IO) { getDestinyProfiles(displayName, displayNameCode, 3) } 20 | 21 | suspend fun searchUsers(criteria: String): Set { 22 | val result = 23 | withContext(Dispatchers.Default) { searchUsersInternal(criteria) } 24 | if (result.isEmpty()) { 25 | throw PlayerNotFoundException("没有搜索到玩家,请检查你的搜索内容") 26 | } 27 | 28 | // Filter Destiny 2 players 29 | val players = mutableSetOf() 30 | result.map { profile -> 31 | GlobalScope.launch { 32 | try { 33 | val destinyMembership = bungieUserToDestinyUser(profile.bungieGlobalDisplayName, profile.bungieGlobalDisplayNameCode) 34 | if (destinyMembership != null) { 35 | players.add(destinyMembership) 36 | } 37 | } catch (e: ConnectTimeoutException) { 38 | throw ConnectTimeoutException("尝试获取玩家 $profile 信息时超时。", e) 39 | } 40 | } 41 | }.joinAll() 42 | return players 43 | } 44 | 45 | suspend fun searchUsersProfile(criteria: String) = 46 | searchUsers(criteria).map { 47 | withContext(Dispatchers.IO) { getProfile(it.membershipType, it.membershipId) } 48 | } 49 | 50 | 51 | suspend fun searchUsersInternal(criteria: String): List = 52 | getJson("$endpoint/User/Search/Prefix/$criteria/0/") { 53 | header("X-API-Key", key) 54 | }.Response?.searchResults!! 55 | 56 | 57 | suspend fun searchProfiles(criteria: String): List = 58 | getJson("$endpoint/Destiny2/SearchDestinyPlayer/TigerSteam/$criteria/ ") { 59 | header("X-API-Key", key) 60 | post("""{"displayNamePrefix":"$criteria"}""".toRequestBody("application/json".toMediaType())) 61 | }.Response 62 | 63 | suspend fun getProfile(membershipType: Int, membershipId: String): DestinyProfile? = 64 | getJson("$endpoint/Destiny2/${membershipType}/Profile/${membershipId}/?components=Profiles%2CCharacters%2CProfileCurrencies") { 65 | header("X-API-Key", key) 66 | }.Response 67 | 68 | suspend fun getCharacter(membershipType: Int, membershipId: String, characterId: String): DestinyCharacterResponseComponent? = 69 | getJson("$endpoint/Destiny2/${membershipType}/Profile/${membershipId}/Character/${characterId}/?components=Characters%2CCharacterInventories%2CCharacterEquipment%2CItemPerks") { 70 | header("X-API-Key", key) 71 | }.Response 72 | 73 | suspend fun getMembershipFromHardLinkedCredential(credential: String, crType: String = "SteamId"): DestinyMembershipQuery? = 74 | getJson("$endpoint/User/GetMembershipFromHardLinkedCredential/${crType}/${credential}/") { 75 | header("X-API-Key", key) 76 | }.Response 77 | 78 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/DestinyExceptions.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | class WeaponNotFoundException(message: String? = null) : Exception(message) 4 | 5 | class PlayerNotFoundException(message: String? = null) : Exception(message) 6 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/DestinyManifestDatabase.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import net.origind.destinybot.features.Database 6 | import net.origind.destinybot.features.destiny.data.Lore 7 | import net.origind.destinybot.features.destiny.response.lightgg.ItemDefinition 8 | import net.origind.destinybot.features.moshi 9 | import org.bson.Document 10 | import org.litote.kmongo.aggregate 11 | import java.util.concurrent.ConcurrentHashMap 12 | 13 | val searchToWeaponMap = ConcurrentHashMap() 14 | 15 | /** 16 | * @throws WeaponNotFoundException if specified item is not found 17 | */ 18 | suspend fun searchItemDefinitions(displayName: String): List { 19 | var itemSearch = displayName 20 | if (searchToWeaponMap.containsKey(itemSearch)) itemSearch = searchToWeaponMap[itemSearch]!! 21 | 22 | val documents = Database.getItemDefinitions(itemSearch) 23 | if (documents.isEmpty()) throw WeaponNotFoundException("无法找到该物品,请检查你的内容并用简体中文译名搜索。") 24 | else return documents 25 | } 26 | 27 | suspend fun getRandomLore() : Lore = withContext(Dispatchers.IO) { 28 | val collection = Database.db.getCollection("DestinyLoreDefinition_chs") 29 | val doc = collection.aggregate("""{${'$'}sample: { size: 1 }}""").firstOrNull() 30 | val displayProperties = doc?.get("displayProperties", Document::class.java) 31 | if (displayProperties?.getString("name").isNullOrEmpty()) return@withContext getRandomLore() 32 | return@withContext moshi.adapter(Lore::class.java).fromJson(displayProperties!!.toJson())!! 33 | } 34 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/LightggAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import kotlinx.coroutines.* 4 | import net.origind.destinybot.features.Database 5 | import net.origind.destinybot.features.destiny.response.lightgg.* 6 | import net.origind.destinybot.features.getJson 7 | import net.origind.destinybot.features.logger 8 | import net.origind.destinybot.features.moshi 9 | import java.nio.charset.StandardCharsets 10 | import java.nio.file.Files 11 | import java.nio.file.Paths 12 | 13 | const val WISHLIST_URL = "https://cdn.jsdelivr.net/gh/LittleLightForDestiny/littlelight_wishlists/littlelight_default.json" 14 | val wishlist: Wishlist = Wishlist() 15 | 16 | class ItemNotFoundException(itemId: String, displayName: String? = null) : Exception("未找到物品 $itemId") 17 | 18 | suspend fun fetchWishlist() { 19 | coroutineScope { 20 | val jobs = mutableListOf() 21 | val wishlistData = getJson(WISHLIST_URL) 22 | for (wishItemData in wishlistData.data) { 23 | jobs += async { 24 | val recommendation = wishItemData.tags.firstOrNull() ?: "" 25 | wishlist.weaponMap.getOrPut(wishItemData.hash) { Wishlist.WishlistItem() }.apply { 26 | wishItemData.plugs.flatten().map { it to recommendation }.forEach { 27 | put(it.first, it.second) 28 | } 29 | } 30 | } 31 | } 32 | jobs.joinAll() 33 | logger.info("Wishlist built") 34 | } 35 | } 36 | 37 | suspend fun getItemPerks(item: ItemDefinition) = getItemPerks(item._id!!) 38 | 39 | suspend fun getItemPerks(itemId: String): ItemPerks = withContext(Dispatchers.IO) { 40 | val dir = Paths.get("destiny2_perks") 41 | val path = dir.resolve("$itemId.json") 42 | 43 | if (Files.notExists(dir)) Files.createDirectories(dir) 44 | if (Files.exists(path)) { 45 | return@withContext moshi.adapter(Item::class.java).fromJson(Files.readString(path, StandardCharsets.UTF_8))?.perks!! 46 | } else { 47 | val perks = getItemPerksInternal(itemId) 48 | val itemDefinition = Database.getItemDefinition(itemId) 49 | Files.writeString(path, moshi.adapter(Item::class.java).toJson(Item(perks, itemDefinition))) 50 | return@withContext perks 51 | } 52 | } 53 | 54 | suspend fun getItemPerksInternal(itemId: String): ItemPerks { 55 | val perks = ItemPerks() 56 | val sockets = getWeaponSockets(itemId) 57 | 58 | sockets.block.socketEntries.asSequence().drop(1).take(4).forEachIndexed { index, socketEntry -> 59 | val plugHashes = if (socketEntry.randomizedPlugSetHash == null) listOf(socketEntry.singleInitialItemHash) else sockets.plugSets[socketEntry.randomizedPlugSetHash]!!.reusablePlugItems.map { it.plugItemHash } 60 | val plugs = plugHashes.map { sockets.plugs[it]!! } 61 | val type = when (index) { 62 | 0 -> PerkType.BARREL 63 | 1 -> PerkType.MAGAZINE 64 | 2 -> PerkType.PERK1 65 | else -> PerkType.PERK2 66 | } 67 | for (plug in plugs) { 68 | perks += ItemPerk(type = type).apply { 69 | url = plug.imageUrl 70 | isCurated = socketEntry.randomizedPlugSetHash == null 71 | perkRecommend = wishlist.getRecommendationForPlug(itemId.toLong(), plug.hash) 72 | displayProperties = Database.translate(plug.name!!) ?: DisplayProperties(name = plug.name) 73 | } 74 | } 75 | } 76 | return perks 77 | } 78 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/LoreCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import kotlinx.coroutines.coroutineScope 4 | import kotlinx.coroutines.launch 5 | import net.origind.destinybot.api.command.* 6 | import net.origind.destinybot.features.Database 7 | import net.origind.destinybot.features.destiny.data.Lore 8 | import net.origind.destinybot.features.moshi 9 | import org.bson.Document 10 | import org.litote.kmongo.findOne 11 | 12 | object LoreCommand: AbstractCommand("传奇故事") { 13 | val lores = hashMapOf() 14 | 15 | init { 16 | arguments += ArgumentContext("lore", StringArgument, true) 17 | } 18 | 19 | override suspend fun init() { 20 | coroutineScope { 21 | launch { 22 | val loreCollection = Database.db.getCollection("DestinyLoreDefinition_chs") 23 | lores.putAll(loreCollection.find().map { it.get("displayProperties", Document::class.java)?.getString("name")!! to it.getString("_id") }) 24 | } 25 | } 26 | } 27 | 28 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 29 | val lore = if (argument.hasArgument("lore")) { 30 | val collection = Database.db.getCollection("DestinyLoreDefinition_chs") 31 | val doc = collection.findOne("""{"_id": "${lores[argument.getArgument("lore")]}"}""") 32 | val displayProperties = doc?.get("displayProperties", Document::class.java) 33 | displayProperties!!.let { 34 | moshi.adapter(Lore::class.java).fromJson(it.toJson())!! 35 | } 36 | } else { 37 | getRandomLore() 38 | } 39 | 40 | executor.sendMessage("传奇故事:" + lore.name + '\n' + lore.lore) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/MyProfileCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import net.origind.destinybot.api.command.* 4 | import net.origind.destinybot.features.DataStore 5 | 6 | object MyProfileCommand: AbstractCommand("我的信息") { 7 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 8 | val user = DataStore[context.senderId] 9 | if (executor !is UserCommandExecutor) return // TODO 统一的executor类型判断 10 | 11 | if (user.destinyMembershipId.isEmpty()) { 12 | if (executor.groupContains(3320645904)) // CY BOT 13 | executor.sendMessage("你还没有绑定账号! 请搜索一个玩家并绑定之。") 14 | } else { 15 | PlayerProfileCommand.replyProfile(user.destinyMembershipType, user.destinyMembershipId, executor) 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/PerkCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import kotlinx.coroutines.coroutineScope 4 | import kotlinx.coroutines.launch 5 | import net.origind.destinybot.api.command.* 6 | import net.origind.destinybot.features.destiny.image.toByteArray 7 | import net.origind.destinybot.features.destiny.image.toImage 8 | import net.origind.destinybot.features.destiny.response.lightgg.ItemDefinition 9 | import net.origind.destinybot.features.destiny.response.lightgg.ItemPerks 10 | 11 | object PerkCommand : AbstractCommand("perk") { 12 | init { 13 | arguments += ArgumentContext("item", StringArgument) 14 | } 15 | 16 | override suspend fun init() { 17 | coroutineScope { 18 | launch { 19 | fetchWishlist() 20 | } 21 | } 22 | } 23 | 24 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 25 | val it = argument.getArgument("item") 26 | if (it.isBlank()) return 27 | for (item in searchItemDefinitions(it)) { 28 | try { 29 | val perks = getItemPerks(item._id!!) 30 | replyPerks(item, perks, executor) 31 | } catch (e: WeaponNotFoundException) { 32 | executor.sendMessage(e.localizedMessage ?: "") 33 | } catch (e: ItemNotFoundException) { 34 | executor.sendMessage("搜索失败: ${e.localizedMessage}, 正在尝试其他方式") 35 | } catch (e: Exception) { 36 | executor.sendMessage("搜索失败:${e.localizedMessage}, 正在尝试其他方式") 37 | } 38 | } 39 | } 40 | 41 | suspend fun replyPerks(item: ItemDefinition, perks: ItemPerks, executor: CommandExecutor) { 42 | 43 | if (executor is UserCommandExecutor) 44 | executor.sendImage(item.toImage(perks).toByteArray()) 45 | executor.sendMessage(buildString { 46 | appendLine("信息来自 Little Light 愿望单") 47 | appendLine(item.displayProperties?.name + " " + item.itemTypeAndTierDisplayName) 48 | appendLine(item.displayProperties?.description) 49 | appendLine() 50 | append("官Roll(可能不掉落): ") 51 | appendLine(perks.curated.joinToString(separator = ", ") { it.displayProperties?.name.toString() }) 52 | if (perks.favorite.isNotEmpty()) { 53 | append("社区精选 Perk: ") 54 | appendLine(perks.favorite.joinToString(separator = ", ") { it.displayProperties?.name.toString() }) 55 | } 56 | if (perks.pvp.isNotEmpty()) { 57 | append("PvP Perk: ") 58 | appendLine(perks.pvp.joinToString(separator = ", ") { it.displayProperties?.name.toString() }) 59 | } 60 | if (perks.pve.isNotEmpty()) { 61 | append("PvE Perk: ") 62 | appendLine(perks.pve.joinToString(separator = ", ") { it.displayProperties?.name.toString() }) 63 | } 64 | if (perks.normal.isNotEmpty()) { 65 | append("其他 Perk: ") 66 | append(perks.normal.joinToString(separator = ", ") { it.displayProperties?.name.toString() }) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/PlayerProfileCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import io.ktor.client.plugins.* 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import net.origind.destinybot.api.command.* 7 | import net.origind.destinybot.features.destiny.image.toByteArray 8 | import net.origind.destinybot.features.destiny.image.toImage 9 | 10 | object PlayerProfileCommand : AbstractCommand("/j") { 11 | init { 12 | arguments += ArgumentContext("steamid", StringArgument) 13 | } 14 | 15 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 16 | val id = argument.getArgument("steamid") 17 | val query = getMembershipFromHardLinkedCredential(id) 18 | if (query == null) { 19 | executor.sendMessage("没有找到用户,请检查你的输入。") 20 | return 21 | } 22 | replyProfile(query.membershipType, query.membershipId, executor) 23 | } 24 | 25 | suspend fun replyProfile(membershipType: Int, membershipId: String, executor: CommandExecutor) { 26 | try { 27 | executor.sendMessage(buildString { 28 | appendLine("Tracker: https://destinytracker.com/destiny-2/profile/steam/${membershipId}/overview") 29 | appendLine("Braytech: https://braytech.org/3/${membershipId}") 30 | append("Raid 报告: https://raid.report/pc/${membershipId}") 31 | }) 32 | val profile = withContext(Dispatchers.IO) { 33 | getProfile(3, membershipId) 34 | } 35 | if (profile == null) 36 | executor.sendMessage("获取详细信息时失败,请重试。") 37 | val userProfile = profile?.profile?.data?.userInfo 38 | val description = buildString { 39 | appendLine("玩家: ${userProfile?.displayName}") 40 | appendLine("ID: ${userProfile?.membershipId}") 41 | } 42 | executor.sendMessage(description) 43 | if (executor is UserCommandExecutor) { 44 | executor.sendImage(profile?.characters?.data?.map { (id, character) -> 45 | character 46 | }?.toImage()?.toByteArray() ?: ByteArray(0)) 47 | } 48 | } catch (e: ServerResponseException) { 49 | executor.sendMessage("获取详细信息时失败,请重试。\n${e.localizedMessage}") 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/QueryLinkedCredentialCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import net.origind.destinybot.api.command.* 4 | 5 | object QueryLinkedCredentialCommand : AbstractCommand("/你给翻译翻译") { 6 | init { 7 | arguments += ArgumentContext("steamid", StringArgument) 8 | } 9 | 10 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 11 | val id = argument.getArgument("steamid") 12 | val query = getMembershipFromHardLinkedCredential(id) 13 | if (query == null) { 14 | executor.sendMessage("你不叫马邦德,我叫马邦德") 15 | return 16 | } 17 | executor.sendMessage("好嘞。\n你的棒鸡ID:${query.membershipId}") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/SearchChooseResultCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import io.ktor.client.plugins.* 4 | import net.origind.destinybot.api.command.AbstractCustomCommand 5 | import net.origind.destinybot.api.command.ArgumentContainer 6 | import net.origind.destinybot.api.command.CommandContext 7 | import net.origind.destinybot.api.command.CommandExecutor 8 | 9 | object SearchChooseResultCommand : AbstractCustomCommand("选择查询结果") { 10 | override suspend fun execute( 11 | main: String, 12 | argument: ArgumentContainer, 13 | executor: CommandExecutor, 14 | context: CommandContext 15 | ) { 16 | if (profileQuerys[context.senderId].isNullOrEmpty()) 17 | return 18 | 19 | val choice = main.toIntOrNull() ?: return 20 | 21 | val result = profileQuerys[context.senderId]!! 22 | val index = choice - 1 23 | if (result.size < index + 1) return 24 | val destinyMembership = result[index] 25 | try { 26 | PlayerProfileCommand.replyProfile( 27 | destinyMembership.membershipType, 28 | destinyMembership.membershipId, 29 | executor 30 | ) 31 | } catch (e: ServerResponseException) { 32 | executor.sendMessage("获取详细信息时失败,请重试。\n${e.localizedMessage}") 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/SearchCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import io.ktor.client.network.sockets.* 4 | import kotlinx.coroutines.* 5 | import net.origind.destinybot.api.command.* 6 | import net.origind.destinybot.features.destiny.response.DestinyMembershipQuery 7 | import java.util.concurrent.ConcurrentHashMap 8 | 9 | val profileQuerys = ConcurrentHashMap>() 10 | 11 | object SearchCommand : AbstractCommand("命运2开盒") { 12 | init { 13 | arguments += ArgumentContext("criteria", StringArgument) 14 | } 15 | 16 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 17 | val criteria = argument.getArgument("criteria").replace("#", "%23") // https://github.com/Bungie-net/api/issues/301 18 | profileQuerys.remove(context.senderId) 19 | val result = 20 | withContext(Dispatchers.Default) { searchUsersInternal(criteria) } 21 | executor.sendMessage("搜索命运2玩家: $criteria") 22 | if (result.isEmpty()) { 23 | executor.sendMessage("没有搜索到玩家,请检查你的搜索内容") 24 | return 25 | } 26 | 27 | // Filter Destiny 2 players 28 | val players = mutableSetOf() 29 | coroutineScope { 30 | result.map { profile -> 31 | launch { 32 | try { 33 | val destinyMembership = bungieUserToDestinyUser(profile.bungieGlobalDisplayName, profile.bungieGlobalDisplayNameCode) 34 | if (destinyMembership != null) { 35 | players.add(destinyMembership) 36 | } 37 | } catch (e: ConnectTimeoutException) { 38 | executor.sendMessage("尝试获取玩家 $profile 信息时超时。") 39 | } 40 | } 41 | }.joinAll() 42 | } 43 | profileQuerys[context.senderId] = players.toList() 44 | executor.sendMessage(buildString { 45 | appendLine("搜索到玩家: ") 46 | players.forEachIndexed { index, profile -> 47 | appendLine("${index + 1}. ${profile}: ...${profile.membershipId.takeLast(3)}") 48 | } 49 | appendLine("请直接回复前面的序号(是1 2 3 不是375 668 451等等等)来获取详细信息。") 50 | appendLine("或者,回复 绑定 [序号] 来将该用户绑定到你的 QQ 上。") 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/TrackerAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import net.origind.destinybot.features.destiny.response.tracker.* 4 | import net.origind.destinybot.features.getJson 5 | 6 | const val trackerEndpoint = "https://api.tracker.gg/api" 7 | const val trackerKey = "a9660274-5674-4ad5-ad87-08e5ec9348a7" 8 | 9 | suspend fun searchTrackerProfiles(query: String, platform: String = "steam"): List { 10 | val response = getJson("$trackerEndpoint/v2/destiny-2/standard/search?platform=$platform&query=$query&autocomplete=true") { 11 | header("TRN-Api-Key", trackerKey) 12 | } 13 | if (!response.errors.isNullOrEmpty()) { 14 | throw TrackerException(response.errors!!) 15 | } 16 | return response.data!! 17 | } 18 | 19 | suspend fun getWeaponSockets(itemId: String): TrackerWeapon { 20 | val response = getJson("$trackerEndpoint/v1/destiny-2/db/items/$itemId/sockets") { 21 | header("TRN-Api-Key", trackerKey) 22 | } 23 | if (!response.errors.isNullOrEmpty()) { 24 | throw TrackerException(response.errors!!) 25 | } 26 | return response.data!! 27 | } 28 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/TrackerCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import net.origind.destinybot.api.command.* 6 | 7 | object TrackerCommand : AbstractCommand("/tracker") { 8 | init { 9 | arguments += ArgumentContext("criteria", StringArgument) 10 | } 11 | 12 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 13 | val criteria = argument.getArgument("criteria") 14 | val result = withContext(Dispatchers.Default) { 15 | searchTrackerProfiles(criteria) 16 | } 17 | executor.sendMessage("搜索Tracker上的命运2玩家: $criteria") 18 | if (result.isEmpty()) { 19 | executor.sendMessage("没有搜索到玩家,请检查你的搜索内容") 20 | return 21 | } 22 | executor.sendMessage(buildString { 23 | appendLine("搜索到玩家: ") 24 | result.forEachIndexed { index, profile -> 25 | appendLine("${index + 1}. ${profile.platformUserHandle}: https://destinytracker.com/destiny-2/profile/${profile.platformSlug}/${profile.platformUserIdentifier}/overview") 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/WeeklyReportCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny 2 | 3 | import net.origind.destinybot.api.command.* 4 | import net.origind.destinybot.features.bilibili.getLatestWeeklyReportURL 5 | import net.origind.destinybot.features.destiny.image.getImage 6 | import net.origind.destinybot.features.destiny.image.toByteArray 7 | 8 | object WeeklyReportCommand: AbstractCommand("周报") { 9 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 10 | if (executor is UserCommandExecutor) { 11 | executor.sendImage(getImage("https:${getLatestWeeklyReportURL()}").toByteArray()) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/data/Lore.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.data 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class Lore(val name: String, @Json(name = "description") val lore: String) 8 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/data/UserData.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.data 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class UserData( 7 | var qq: Long = -1L, 8 | var bungieMembershipId: String = "", 9 | var destinyMembershipType: Int = 3, 10 | var destinyMembershipId: String = "", 11 | var destinyDisplayName: String = "", 12 | var isAdmin: Boolean = false 13 | ) 14 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/image/DestinyImageCache.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.image 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import net.origind.destinybot.features.client 6 | import net.origind.destinybot.features.logger 7 | import okhttp3.Request 8 | import java.awt.image.BufferedImage 9 | import java.io.File 10 | import java.io.IOException 11 | import java.nio.file.Files 12 | import java.nio.file.Paths 13 | import java.nio.file.StandardOpenOption 14 | import javax.imageio.ImageIO 15 | 16 | /** 17 | * @param icon icon path in displayProperties 18 | * @see net.origind.destinybot.features.destiny.response.lightgg.DisplayProperties.icon 19 | */ 20 | suspend fun getImage(icon: String): BufferedImage { 21 | try { 22 | return ImageIO.read(getImageFile(icon)) 23 | } catch (e: IOException) { 24 | logger.warn("Cannot read image file $icon", e) 25 | throw e 26 | } 27 | } 28 | 29 | suspend fun getImageFile(icon: String): File = withContext(Dispatchers.IO) { 30 | // Check Directory 31 | val dir = Paths.get(when { 32 | icon.startsWith("/common/destiny2_content/icons") -> "destiny2_icons" 33 | icon.startsWith("https://titles.trackercdn.com/destiny/common/destiny2_content/icons") -> "destiny2_icons" 34 | icon.startsWith("/common/destiny2_content/screenshots") -> "destiny2_screenshots" 35 | else -> "destiny2_images" 36 | }).toAbsolutePath() 37 | val fileName = icon.substringAfterLast('/') 38 | val path = dir.resolve(fileName) 39 | 40 | if (Files.notExists(dir)) Files.createDirectories(dir) 41 | if (Files.exists(path)) { 42 | return@withContext path.toFile() 43 | } 44 | 45 | 46 | val request = Request.Builder().apply { 47 | url(if (icon.startsWith("http")) icon else "https://www.bungie.net$icon") 48 | }.build() 49 | 50 | val response = client.newCall(request).execute() 51 | Files.write(path, response.body?.bytes()!!, StandardOpenOption.WRITE, StandardOpenOption.CREATE) 52 | 53 | return@withContext path.toFile() 54 | } 55 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/image/ImageHelper.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.image 2 | 3 | import java.awt.image.RenderedImage 4 | import java.io.ByteArrayOutputStream 5 | import javax.imageio.ImageIO 6 | 7 | fun RenderedImage.toByteArray(): ByteArray { 8 | val stream = ByteArrayOutputStream() 9 | ImageIO.write(this, "png", stream) 10 | return stream.toByteArray() 11 | } 12 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/BungieMultiResponse.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | open class BungieMultiResponse : DestinyMessageResponse() { 7 | var Response: List = emptyList() 8 | override fun toString(): String { 9 | return "MultiResponse(Response=$Response) ${super.toString()}" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/BungieResponses.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class DestinySearchUsersResponse(var searchResults: List = emptyList()) 7 | 8 | @JsonClass(generateAdapter = true) 9 | data class GeneralUser(var bungieNetMembershipId: String = "", var bungieGlobalDisplayName: String = "", var bungieGlobalDisplayNameCode: Int = 0, 10 | var firstAccess: String = "", var destinyMemberships: List = emptyList()) { 11 | override fun toString(): String = "$bungieGlobalDisplayName#$bungieGlobalDisplayNameCode" 12 | } 13 | 14 | 15 | @JsonClass(generateAdapter = true) 16 | data class UserMembershipData(var bungieNetUser: GeneralUser = GeneralUser(), var destinyMemberships: List = emptyList()) 17 | 18 | @JsonClass(generateAdapter = true) 19 | class GetMembershipsResponse : BungieMultiResponse() 20 | 21 | @JsonClass(generateAdapter = true) 22 | class UserSearchResponse : SingleResponse() 23 | 24 | @JsonClass(generateAdapter = true) 25 | data class DestinyMembershipQuery(var membershipType: Int = 0, var membershipId: String = "", var displayName: String = "", var isPublic: Boolean = false, var bungieGlobalDisplayName: String = "", var bungieGlobalDisplayNameCode: Int = 0) { 26 | override fun equals(other: Any?): Boolean { 27 | if (this === other) return true 28 | if (other !is DestinyMembershipQuery) return false 29 | 30 | if (membershipType != other.membershipType) return false 31 | if (membershipId != other.membershipId) return false 32 | 33 | return true 34 | } 35 | 36 | override fun hashCode(): Int { 37 | var result = membershipType 38 | result = 31 * result + membershipId.hashCode() 39 | return result 40 | } 41 | 42 | override fun toString(): String = "$displayName ($bungieGlobalDisplayName#$bungieGlobalDisplayNameCode)" 43 | } 44 | 45 | @JsonClass(generateAdapter = true) 46 | class DestinyProfileSearchResponse : BungieMultiResponse() 47 | 48 | @JsonClass(generateAdapter = true) 49 | data class DestinyProfileComponent(var userInfo: DestinyMembershipQuery = DestinyMembershipQuery(), var characterIds: List = emptyList(), var dateLastPlayed: String = "") 50 | 51 | @JsonClass(generateAdapter = true) 52 | open class CharacterComponent( 53 | var dateLastPlayed: String = "", var minutesPlayedThisSession: Long = 0, var minutesPlayedTotal: Long = 0, 54 | var light: Int = 0, var stats: Map = emptyMap(), 55 | var raceType: Int = 3, var classType: Int = 3, var genderType: Int = 2, 56 | var emblemBackgroundPath: String = "" 57 | ) 58 | 59 | @JsonClass(generateAdapter = true) 60 | data class DestinyProfile(var profile: PrivacyData = PrivacyData(), var characters: PrivacyData> = PrivacyData()) 61 | 62 | @JsonClass(generateAdapter = true) 63 | data class DestinyCharacterResponseComponent(var character: PrivacyData = PrivacyData(), var itemComponents: DestinyItemComponentSetOfint64 = DestinyItemComponentSetOfint64(), 64 | var equipment: PrivacyData = PrivacyData() 65 | ) 66 | 67 | @JsonClass(generateAdapter = true) 68 | data class DestinyItemPerkComponent(var perkHash: String = "", var iconPath: String = "", var isActive: Boolean = false, var visible: Boolean = true) 69 | @JsonClass(generateAdapter = true) 70 | data class DestinyItemComponent(var itemHash: Long = 0, var itemInstanceId: String = "", var quantity: Int = 0, var bindStatus: Int = 0, 71 | var location: Int = 0, var bucketHash: Long = 0, var transferStatus: Int = 0) 72 | 73 | @JsonClass(generateAdapter = true) 74 | data class DestinyItemPerksComponent(var perks: List = emptyList()) 75 | 76 | @JsonClass(generateAdapter = true) 77 | data class DestinyItemsComponent(var items: List = emptyList()) 78 | 79 | @JsonClass(generateAdapter = true) 80 | data class DestinyItemComponentSetOfint64(var perks: PrivacyData> = PrivacyData()) 81 | 82 | @JsonClass(generateAdapter = true) 83 | class DestinyProfileResponse : SingleResponse() 84 | 85 | @JsonClass(generateAdapter = true) 86 | class DestinyCharacterResponse : SingleResponse() 87 | 88 | @JsonClass(generateAdapter = true) 89 | class DestinyMembershipQueryResponse: SingleResponse() 90 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/BungieSingleResponse.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | open class SingleResponse : DestinyMessageResponse() { 7 | var Response: T? = null 8 | override fun toString(): String { 9 | return "SingleResponse(Response=$Response) ${super.toString()}" 10 | } 11 | } 12 | 13 | @JsonClass(generateAdapter = true) 14 | data class PrivacyData(var data: T? = null, var privacy: Int = 1) 15 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/DestinyActivityDefinition.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response 2 | 3 | import net.origind.destinybot.features.destiny.response.lightgg.DisplayProperties 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class DestinyActivityDefinition(val hash: Long, val displayProperties: DisplayProperties, val originalDisplayProperties: DisplayProperties?, val selectionDisplayProperties: DisplayProperties?) 8 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/DestinyMessageResponse.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | open class DestinyMessageResponse { 7 | var ErrorCode: Int = 1 8 | var ThrottleSeconds: Int = 0 9 | var ErrorStatus: String = "" 10 | var Message: String = "" 11 | var MessageData: Map = emptyMap() 12 | override fun toString(): String { 13 | return "DestinyMessageResponse(ErrorCode=$ErrorCode, ThrottleSeconds=$ThrottleSeconds, ErrorStatus='$ErrorStatus', Message='$Message', MessageData=${MessageData})" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/lightgg/Perks.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response.lightgg 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | enum class PerkType : Comparable { 6 | BARREL, MAGAZINE, PERK1, PERK2 7 | } 8 | 9 | enum class ItemTier { 10 | LEGENDARY, EXOTIC, OTHER 11 | } 12 | 13 | @JsonClass(generateAdapter = true) 14 | data class DisplayProperties(var description: String? = "", var name: String? = "", var icon: String? = "") 15 | 16 | @JsonClass(generateAdapter = true) 17 | data class Item(var perks: ItemPerks? = null, var definition: ItemDefinition? = null) 18 | 19 | @JsonClass(generateAdapter = true) 20 | data class ItemDefinition(var _id: String? = "", var screenshot: String? = "", var itemTypeAndTierDisplayName: String? = "", var displayProperties: DisplayProperties? = DisplayProperties()) { 21 | val tier: ItemTier get() = when { 22 | itemTypeAndTierDisplayName?.contains("传说") == true -> ItemTier.LEGENDARY 23 | itemTypeAndTierDisplayName?.contains("异域") == true -> ItemTier.EXOTIC 24 | else -> ItemTier.OTHER 25 | } 26 | } 27 | 28 | @JsonClass(generateAdapter = true) 29 | data class ItemPerks(var curated: List = emptyList(), var favorite: List = emptyList(), 30 | var pvp: List = emptyList(), var pve: List = emptyList(), var normal: List = emptyList(), var onlyCurated: Boolean = false) { 31 | operator fun plusAssign(perk: ItemPerk) { 32 | if (perk.isCurated) curated += perk 33 | else when(perk.perkRecommend) { 34 | -1 -> normal += perk 35 | 0 -> pve += perk 36 | 1 -> pvp += perk 37 | 2 -> favorite += perk 38 | } 39 | } 40 | val all get() = (favorite.asSequence() + pvp + pve + normal).sortedBy { -it.perkRecommend } 41 | } 42 | 43 | @JsonClass(generateAdapter = true) 44 | data class ItemPerk(var displayProperties: DisplayProperties? = DisplayProperties(), var isCurated: Boolean = false, 45 | var _id: String? = "", var type: PerkType? = null, var itemTypeDisplayName: String? = null, var perkRecommend: Int = -1, var url: String? = null) 46 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/lightgg/Wishlist.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response.lightgg 2 | 3 | import com.squareup.moshi.JsonClass 4 | import java.util.concurrent.ConcurrentHashMap 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class WishlistData(val data: List) 8 | 9 | @JsonClass(generateAdapter = true) 10 | data class WishlistItemData(val description: String = "", val plugs: List>, val hash: Long, val tags: List, val originalWishList: String = "") 11 | 12 | data class Wishlist(val weaponMap: MutableMap = ConcurrentHashMap()) { 13 | data class WishlistItem( 14 | // Key: Plug Hash Value: Recommendation 15 | val recommendationMap: MutableMap = mutableMapOf() 16 | ) { 17 | fun put(perkHash: Long, recommendation: String) { 18 | if (recommendation.startsWith("god")) recommendationMap[perkHash] = recommendation 19 | else if (recommendationMap[perkHash] != null && recommendationMap[perkHash] != recommendation) recommendationMap[perkHash] = "favorite" 20 | else recommendationMap[perkHash] = recommendation 21 | } 22 | } 23 | 24 | fun getRecommendationForPlug(weaponHash: Long, plugHash: Long) = when (weaponMap[weaponHash]?.recommendationMap?.get(plugHash)) { 25 | "pve" -> 0 26 | "pvp" -> 1 27 | "godpve" -> 2 28 | "godpvp" -> 2 29 | "favorite" -> 2 30 | else -> -1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/tracker/TrackerResponses.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response.tracker 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | open class TrackerResponse(var data: T? = null, var errors: List? = null) 7 | 8 | @JsonClass(generateAdapter = true) 9 | data class TrackerError(val code: Int, val message: String) 10 | 11 | class TrackerException(val error: List) : Exception(error.joinToString { it.message + "\n" }) 12 | 13 | data class BaseTrackerProfile( 14 | var platformId: Int = 0, var platformSlug: String = "", var platformUserIdentifier: String = "", var platformUserHandle: String = "", 15 | var avatarUrl: String = "" 16 | ) 17 | 18 | data class TrackerCharacterAttribute(var mobility: Int = 0, var resilience: Int = 0, var recovery: Int = 0) 19 | 20 | data class TrackerCharacterMetadata(var backgroundImage: String = "", var emblemImage: String = "", var lightLevel: String = "", 21 | var level: String = "", @Json(name = "class") var classType: String = "", var race: String = "") 22 | 23 | data class TrackerCharacter(var id: String = "", var activeCharacter: Boolean = false, 24 | var metadata: TrackerCharacterMetadata = TrackerCharacterMetadata(), 25 | var attributes: TrackerCharacterAttribute = TrackerCharacterAttribute() 26 | ) 27 | 28 | class TrackerProfileResponse : TrackerResponse>() 29 | 30 | class TrackerCharactersResponse : TrackerResponse>() 31 | 32 | class TrackerWeaponResponse : TrackerResponse() 33 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/destiny/response/tracker/TrackerWeapon.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.destiny.response.tracker 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class TrackerWeapon(val block: Block, val socketTypes: Map, val plugs: Map, val plugSets: Map) { 7 | @JsonClass(generateAdapter = true) 8 | data class Block(val socketEntries: List, val intrinsicSockets: List, val socketCategories: List) { 9 | @JsonClass(generateAdapter = true) 10 | data class SocketEntry(val socketTypeHash: Long, val singleInitialItemHash: Long, val reusablePlugItems: List, 11 | val plugSources: Int, val reusablePlugSetHash: Long?, val randomizedPlugSetHash: Long?, val defaultVisible: Boolean) 12 | 13 | @JsonClass(generateAdapter = true) 14 | data class IntrinsicSocket(val plugItemHash: Long, val socketTypeHash: Long, val defaultVisible: Boolean) 15 | 16 | @JsonClass(generateAdapter = true) 17 | data class SocketCategory(val socketCategoryHash: Long, val socketIndexes: List) 18 | } 19 | @JsonClass(generateAdapter = true) 20 | data class SocketType(val hash: Long, val index: Int, val socketCategoryHash: Long) 21 | 22 | @JsonClass(generateAdapter = true) 23 | data class Plug(val hash: Long, val plugCategoryHash: Long, val perks: List, val name: String?, val imageUrl: String?) { 24 | @JsonClass(generateAdapter = true) 25 | data class Perk(val hash: Long, val visibility: String?, val name: String?) 26 | } 27 | 28 | @JsonClass(generateAdapter = true) 29 | data class PlugSet(val hash: Long, val index: Int, val isFakePlugSet: Boolean, val reusablePlugItems: List) 30 | 31 | @JsonClass(generateAdapter = true) 32 | data class PlugItem(val plugItemHash: Long) 33 | } 34 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/github/GitHubCache.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.github 2 | 3 | import net.origind.destinybot.api.cache.Cache 4 | 5 | class GitHubCache : Cache() { 6 | } 7 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/github/GitHubCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.github 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | 8 | object GitHubCommand : AbstractCommand("/github") { 9 | init { 10 | registerSubcommand(GitHubCommitCommand) 11 | } 12 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 13 | executor.sendMessage(getHelp()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/github/GitHubCommit.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.github 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class CommitInfo( 8 | @Json(name = "sha") var sha: String, 9 | @Json(name = "node_id") var nodeId: String, 10 | @Json(name = "commit") var commit: Commit, 11 | @Json(name = "url") var url: String, 12 | @Json(name = "html_url") var htmlUrl: String, 13 | @Json(name = "comments_url") var commentsUrl: String, 14 | @Json(name = "author") var author: Author, 15 | @Json(name = "committer") var committer: Committer, 16 | @Json(name = "parents") var parents: List = arrayListOf() 17 | 18 | ) 19 | 20 | data class CommitAuthor( 21 | @Json(name = "name") var name: String, 22 | @Json(name = "email") var email: String, 23 | @Json(name = "date") var date: String 24 | ) 25 | 26 | data class Tree( 27 | @Json(name = "sha") var sha: String, 28 | @Json(name = "url") var url: String 29 | 30 | ) 31 | 32 | 33 | data class Verification( 34 | @Json(name = "verified") var verified: Boolean, 35 | @Json(name = "reason") var reason: String, 36 | @Json(name = "signature") var signature: String, 37 | @Json(name = "payload") var payload: String 38 | ) 39 | 40 | 41 | data class Commit( 42 | @Json(name = "author") var author: GitAuthor, 43 | @Json(name = "committer") var committer: GitAuthor, 44 | @Json(name = "message") var message: String, 45 | @Json(name = "tree") var tree: Tree, 46 | @Json(name = "url") var url: String, 47 | @Json(name = "comment_count") var commentCount: Int, 48 | // @Json(name = "verification") var verification: Verification 49 | 50 | ) 51 | 52 | data class GitAuthor( 53 | var name: String, 54 | var email: String, 55 | var date: String 56 | ) 57 | 58 | data class Author( 59 | @Json(name = "login") var login: String, 60 | @Json(name = "id") var id: Int, 61 | @Json(name = "node_id") var nodeId: String, 62 | @Json(name = "avatar_url") var avatarUrl: String, 63 | @Json(name = "gravatar_id") var gravatarId: String, 64 | @Json(name = "url") var url: String, 65 | @Json(name = "html_url") var htmlUrl: String, 66 | @Json(name = "followers_url") var followersUrl: String, 67 | @Json(name = "following_url") var followingUrl: String, 68 | @Json(name = "gists_url") var gistsUrl: String, 69 | @Json(name = "starred_url") var starredUrl: String, 70 | @Json(name = "subscriptions_url") var subscriptionsUrl: String, 71 | @Json(name = "organizations_url") var organizationsUrl: String, 72 | @Json(name = "repos_url") var reposUrl: String, 73 | @Json(name = "events_url") var eventsUrl: String, 74 | @Json(name = "received_events_url") var receivedEventsUrl: String, 75 | @Json(name = "type") var type: String, 76 | @Json(name = "site_admin") var siteAdmin: Boolean 77 | 78 | ) 79 | 80 | data class Committer( 81 | @Json(name = "login") var login: String, 82 | @Json(name = "id") var id: Int, 83 | @Json(name = "avatar_url") var avatarUrl: String, 84 | @Json(name = "url") var url: String, 85 | @Json(name = "html_url") var htmlUrl: String, 86 | ) 87 | 88 | data class Parents( 89 | @Json(name = "sha") var sha: String, 90 | @Json(name = "url") var url: String, 91 | @Json(name = "html_url") var htmlUrl: String 92 | ) 93 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/github/GitHubCommitCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.github 2 | 3 | import com.squareup.moshi.Types 4 | import net.origind.destinybot.api.command.* 5 | import net.origind.destinybot.features.getBodyAsync 6 | import net.origind.destinybot.features.moshi 7 | 8 | object GitHubCommitCommand : AbstractCommand("commit") { 9 | val regex = Regex("[\\w\\-_]+/[\\w\\-_]+") 10 | val type = Types.newParameterizedType(List::class.java, CommitInfo::class.java) 11 | 12 | init { 13 | arguments += ArgumentContext("repo", StringArgument) 14 | arguments += ArgumentContext("count", IntArgument, true) 15 | } 16 | 17 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 18 | val repo = argument.getArgument("repo") 19 | if (!repo.matches(regex)) { 20 | executor.sendMessage("仓库名非法") 21 | return 22 | } 23 | 24 | val count = argument.getArgument("count") ?: 5 25 | val infos = moshi.adapter>(type).fromJson(getBodyAsync("https://api.github.com/repos/$repo/commits?per_page=$count").await())!! 26 | executor.sendMessage("GitHub: ${repo}\n展示最后${count}条提交:\n" + infos.joinToString("\n", transform = this::formatInfo)) 27 | } 28 | 29 | fun formatInfo(info: CommitInfo) = 30 | buildString { 31 | appendLine(info.commit.author.name + ": " + info.commit.message) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/injdk/InjdkCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.injdk 2 | 3 | import kotlinx.coroutines.coroutineScope 4 | import kotlinx.coroutines.launch 5 | import net.origind.destinybot.api.command.* 6 | import net.origind.destinybot.features.getBodyAsync 7 | 8 | object InjdkCommand: AbstractCommand("injdk") { 9 | var jreDistro = mapOf>() 10 | var jdkDistro = mapOf>() 11 | 12 | private val jdkRegex = Regex("""(.+)""") 13 | private val jreRegex = Regex(""" = listOf("java下载") 16 | 17 | val translateMap = mapOf("adoptopenjdk" to "AdoptOpenJDK", "jdk" to "OpenJDK", "redhat" to "Red Hat", "sapmachine" to "SAP SE", "openjdk" to "OpenJDK", "zulu" to "Zulu", "liberica" to "Liberica", "amazon" to "Amazon", "mc" to "Microsoft", "openj9" to "IBM OpenJ9", "oracle" to "Oracle") 18 | 19 | init { 20 | arguments += ArgumentContext("version", IntArgument, true) 21 | arguments += ArgumentContext("distro", StringArgument, true) 22 | arguments += ArgumentContext("jdk", StringArgument, true) 23 | } 24 | 25 | override suspend fun init() { 26 | reload() 27 | } 28 | 29 | override fun getHelp(): String = buildString { 30 | appendLine("提供最新的 Java 下载。") 31 | appendLine("用法:<版本> [提供商] [jre|jdk]") 32 | appendLine() 33 | appendLine("示例:injdk 8 - 提供 OpenJDK 1.8 下载") 34 | appendLine("示例:injdk 17 zulu jdk - 提供 Zulu 17 JDK 下载") 35 | } 36 | 37 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 38 | if (argument.hasArgument("version")) { 39 | var company = if (argument.hasArgument("distro")) argument.getArgument("distro") else "openjdk" 40 | 41 | if (company == "adopt") { 42 | company = "adoptopenjdk" 43 | } else if (company == "jdk") { 44 | company = "openjdk" 45 | } 46 | 47 | val jdk = if (argument.hasArgument("jdk")) argument.getArgument("jdk") else "jre" 48 | if (company !in translateMap) executor.sendMessage("未知提供商 $company,可用提供商:${translateMap.keys.joinToString()}") 49 | if (jdk != "jre" && jdk != "jdk") executor.sendMessage("请选择 JDK 或 JRE 其中之一。") 50 | var version = argument.getArgument("version").toString() 51 | if (version == "1.8") version = "8" 52 | val distros = (if (jdk == "jdk") jdkDistro else jreDistro)[company] ?: emptyList() 53 | if (distros.isEmpty()) { 54 | executor.sendMessage("未找到这样的发行版。") 55 | } else { 56 | val distro = distros.filter { it.version == version } 57 | if (distro.isNotEmpty()) { 58 | executor.sendMessage(buildString { 59 | appendLine("${translateMap[company]} Java $version 下载:") 60 | appendLine("Windows x64:${distro.find { it.program.contains("win") && it.program.contains("64") && it.program.contains("msi") }?.url ?: distro.find { it.program.contains("win") && it.program.contains("64") && it.program.contains("zip") }?.url}") 61 | 62 | distro.find { it.program.contains("win") && it.program.contains("86") && it.program.contains("msi") }?.url ?: distro.find { it.program.contains("win") && it.program.contains("86") && it.program.contains("zip") } 63 | ?.url 64 | ?.let { appendLine("Windows x32:$it") } 65 | }) 66 | } 67 | } 68 | } else { 69 | executor.sendMessage(getHelp()) 70 | } 71 | } 72 | 73 | suspend fun reload() { 74 | coroutineScope { 75 | launch { 76 | val jdkHtml = getBodyAsync("https://www.injdk.cn/").await() 77 | jdkDistro = jdkRegex.findAll(jdkHtml).map { it.groupValues }.map { 78 | val group = it[2] 79 | var distro = it[3].removeSuffix("/") 80 | val ext = it[5].removeSuffix("/") 81 | if (ext == "openj9") distro = ext 82 | if (group == "oraclejdk") distro = "oracle" 83 | InjdkDistribution(it[1], distro, it[4], ext, it[6]) 84 | }.groupBy { it.company } 85 | println("JDK Distributions loaded") 86 | } 87 | launch { 88 | val jreHtml = getBodyAsync("https://d2.injdk.cn/jre.html").await() 89 | jreDistro = jreRegex.findAll(jreHtml).map { it.groupValues }.map { 90 | val group = it[2] 91 | var distro = it[3].removeSuffix("/") 92 | val ext = it[5].removeSuffix("/") 93 | if (ext == "openj9") distro = ext 94 | if (group == "oraclejdk") distro = "oracle" 95 | InjdkDistribution(it[1], distro, it[4], ext, it[1].substringAfterLast('/')) 96 | }.groupBy { it.company } 97 | println("JRE Distributions loaded") 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/injdk/InjdkDistribution.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.injdk 2 | 3 | data class InjdkDistribution(val url: String, val company: String, val version: String, val ext: String, val program: String) { 4 | } 5 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/instatus/InstatusAPI.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.instatus 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | 5 | object InstatusAPI { 6 | var apiKey: String? = null 7 | 8 | fun reloadConfig(config: Config) { 9 | apiKey = config.get("instatus.apiKey") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/instatus/InstatusCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.instatus 2 | 3 | import net.origind.destinybot.api.command.AbstractCommand 4 | import net.origind.destinybot.api.command.ArgumentContainer 5 | import net.origind.destinybot.api.command.CommandContext 6 | import net.origind.destinybot.api.command.CommandExecutor 7 | 8 | object InstatusCommand : AbstractCommand("/instatus") { 9 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 10 | if (InstatusAPI.apiKey == null) { 11 | executor.sendMessage("未指定 Instatus API Key。") 12 | return 13 | } 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/instatus/response/Component.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.instatus.response 2 | 3 | data class Component(val id: String, val name: String, val description: String, val status: String, val order: Int) : Comparable { 4 | override fun compareTo(other: Component): Int = order.compareTo(other.order) 5 | } 6 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/instatus/response/Page.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.instatus.response 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class Page(val id: String, val subdomain: String, val name: String, val status: String) 7 | 8 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/minecraft/MinecraftServerAddressArgument.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.minecraft 2 | 3 | import net.origind.destinybot.api.command.ArgumentParseException 4 | import net.origind.destinybot.api.command.ArgumentType 5 | import java.net.InetSocketAddress 6 | 7 | object MinecraftServerAddressArgument : ArgumentType { 8 | override val clazz: Class = InetSocketAddress::class.java 9 | 10 | fun resolveInetAddress(address: String): InetSocketAddress { 11 | return if (address.contains(':')) { 12 | InetSocketAddress(address.substringBefore(':'), Integer.parseInt(address.substringAfter(':'))) 13 | } else { 14 | val socketAddress = InetSocketAddress(address.substringBefore(':'), 25565) 15 | if (socketAddress.isUnresolved) 16 | throw ArgumentParseException("未知服务器") 17 | socketAddress 18 | } 19 | } 20 | 21 | fun isServer(address: String): Boolean { 22 | return address.contains(':') || minecraftConfig.servers.containsKey(address.lowercase()) || !InetSocketAddress(address, 25565).isUnresolved 23 | } 24 | 25 | override fun parse(literal: String): InetSocketAddress { 26 | return if (minecraftConfig.servers.containsKey(literal.lowercase())) { 27 | val spec = minecraftConfig.servers[literal.lowercase()] 28 | InetSocketAddress(spec!!.host, spec.port) 29 | } else { 30 | resolveInetAddress(literal) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/minecraft/MinecraftSpec.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.minecraft 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | 5 | class MinecraftConfig(config: Config) { 6 | val default: MinecraftServerSpec = config.get("minecraft.default").let { 7 | if(it == null) { 8 | MinecraftServerSpec(null) 9 | } else { 10 | val address = MinecraftServerAddressArgument.resolveInetAddress(it) 11 | MinecraftServerSpec(address.hostString, address.port) 12 | } 13 | } 14 | val servers: Map = 15 | (config.get("minecraft.servers")?.valueMap() ?: emptyMap()) 16 | .map { 17 | val address = MinecraftServerAddressArgument.resolveInetAddress(it.value.toString()) 18 | it.key.lowercase() to MinecraftServerSpec(address.hostString, address.port) 19 | }.toMap() 20 | val ignoreCase: Boolean = config.getOrElse("minecraft.ignoreCase", true) 21 | val timeoutMillis: Long = config.getLongOrElse("minecraft.timeout", 3000) 22 | } 23 | 24 | data class MinecraftServerSpec(var host: String? = null, var port: Int = 25565) 25 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/minecraft/MinecraftVersionCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.minecraft 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.coroutineScope 5 | import kotlinx.coroutines.launch 6 | import net.origind.destinybot.api.command.AbstractCustomCommand 7 | import net.origind.destinybot.api.command.ArgumentContainer 8 | import net.origind.destinybot.api.command.CommandContext 9 | import net.origind.destinybot.api.command.CommandExecutor 10 | import net.origind.destinybot.features.getBodyAsync 11 | import net.origind.destinybot.features.moshi 12 | import java.nio.file.Files 13 | import java.nio.file.Paths 14 | import java.time.* 15 | 16 | object MinecraftVersionCommand: AbstractCustomCommand("mc版本") { 17 | var versionManifest: MinecraftVersionManifest 18 | var versionMap: Map 19 | 20 | init { 21 | versionManifest = moshi.adapter(MinecraftVersionManifest::class.java).fromJson(Files.readString(Paths.get("version_manifest.json")))!! 22 | versionMap = versionManifest.versions?.associate { "/" + it.id!!.replace(".", "") to it } ?: emptyMap() 23 | } 24 | 25 | suspend fun reload() = coroutineScope { 26 | launch { 27 | val str = getBodyAsync("https://launchermeta.mojang.com/mc/game/version_manifest.json").await() 28 | launch(Dispatchers.IO) { 29 | versionManifest = moshi.adapter(MinecraftVersionManifest::class.java).fromJson(str)!! 30 | versionMap = versionManifest.versions?.associate { "/" + it.id!!.replace(".", "") to it } ?: emptyMap() 31 | } 32 | launch(Dispatchers.IO) { Files.writeString(Paths.get("version_manifest.json"), str)} 33 | }.join() 34 | } 35 | 36 | private fun buildMinecraftVersionMessage(version: String, builder: StringBuilder) { 37 | val version = if (version in versionMap) versionMap[version] else versionMap["/" + version.replace(".", "").replace("/", "")] 38 | val now = LocalDateTime.now() 39 | val duration = Duration.between(Instant.parse(version?.releaseTime), Instant.now()) 40 | val period = Period.between(ZonedDateTime.parse(version?.releaseTime).toLocalDate(), now.toLocalDate()) 41 | 42 | builder.apply { 43 | append("Minecraft ${version?.id} 已经发布 ") 44 | if (period.years > 0) append("${period.years} 年 ") 45 | if (period.months > 0) append("${period.months} 月 ") 46 | append("${period.days} 天 ${duration.toHoursPart()} 小时 ${duration.toMinutesPart()} 分钟 ${duration.toSecondsPart()} 秒 ${duration.toMillisPart()} 毫秒了。") 47 | } 48 | } 49 | 50 | override suspend fun execute( 51 | main: String, 52 | argument: ArgumentContainer, 53 | executor: CommandExecutor, 54 | context: CommandContext 55 | ) { 56 | if (versionMap.containsKey(main)) { 57 | val builder = StringBuilder() 58 | buildMinecraftVersionMessage(main, builder) 59 | executor.sendMessage(builder.toString()) 60 | return 61 | } 62 | when (main) { 63 | "/release" -> { 64 | val builder = StringBuilder("最新版本 ") 65 | buildMinecraftVersionMessage( 66 | versionManifest.latest?.release!!, 67 | builder 68 | ) 69 | executor.sendMessage(builder.toString()) 70 | } 71 | "/latest" -> { 72 | val builder = if (versionManifest.latest?.snapshot == versionManifest.latest?.release) { 73 | StringBuilder("最新版本 ") 74 | } 75 | else { 76 | StringBuilder("最新预览版 ") 77 | } 78 | buildMinecraftVersionMessage( 79 | versionManifest.latest?.snapshot!!, 80 | builder 81 | ) 82 | executor.sendMessage(builder.toString()) 83 | } 84 | } 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/minecraft/MinecraftVersionManifest.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.minecraft 2 | 3 | data class LatestManifest(var release: String? = null, var snapshot: String? = null) 4 | data class Version(var id: String? = null, var type: String? = null, var url: String? = null, var time: String? = null, var releaseTime: String? = null) 5 | 6 | data class MinecraftVersionManifest(var latest: LatestManifest? = null, var versions: List? = null) 7 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/minecraft/PingCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.minecraft 2 | 3 | import com.electronwill.nightconfig.core.Config 4 | import com.github.steveice10.mc.protocol.MinecraftConstants 5 | import com.github.steveice10.mc.protocol.MinecraftProtocol 6 | import com.github.steveice10.mc.protocol.data.status.ServerStatusInfo 7 | import com.github.steveice10.mc.protocol.data.status.handler.ServerInfoHandler 8 | import com.github.steveice10.mc.protocol.data.status.handler.ServerPingTimeHandler 9 | import com.github.steveice10.packetlib.tcp.TcpClientSession 10 | import com.projecturanus.suffixtree.GeneralizedSuffixTree 11 | import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap 12 | import kotlinx.coroutines.* 13 | import net.kyori.adventure.text.Component 14 | import net.kyori.adventure.text.TextComponent 15 | import net.origind.destinybot.api.command.* 16 | 17 | lateinit var minecraftConfig: MinecraftConfig 18 | 19 | object PingCommand: AbstractCommand("/ping") { 20 | var searchTree = GeneralizedSuffixTree() 21 | val searchTreeResultMap = Int2ObjectAVLTreeMap() 22 | val statusProtocol = MinecraftProtocol() 23 | 24 | override val aliases: List 25 | get() = listOf("ping") 26 | 27 | init { 28 | arguments += ArgumentContext("server", StringArgument, true) 29 | } 30 | 31 | fun reloadConfig(config: Config) { 32 | searchTreeResultMap.clear() 33 | searchTree = GeneralizedSuffixTree() 34 | minecraftConfig.servers.keys.forEachIndexed { index, s -> 35 | searchTree.put(s, index) 36 | searchTreeResultMap[index] = s 37 | } 38 | } 39 | 40 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 41 | try { 42 | if (argument.hasArgument("server")) { 43 | val server = argument.getArgument("server") 44 | if (MinecraftServerAddressArgument.isServer(server)) { 45 | val server = MinecraftServerAddressArgument.parse(server) 46 | executor.sendMessage("正在测试到服务器 $server 的延迟") 47 | try { 48 | withTimeout(minecraftConfig.timeoutMillis) { status(executor, server.hostString, server.port) } 49 | } catch (e: TimeoutCancellationException) { 50 | executor.sendMessage("服务器连接超时") 51 | } catch (e: Exception) { 52 | executor.sendMessage("连接失败: " + e.localizedMessage) 53 | } 54 | } else { 55 | val results = searchTree.search(server) 56 | if (results.isEmpty()) { 57 | executor.sendMessage("未找到服务器 $server") 58 | } else { 59 | executor.sendMessage("未找到服务器 $server,类似的服务器有 ${results.joinToString { searchTreeResultMap[it] }}") 60 | } 61 | } 62 | } else { 63 | if(minecraftConfig.default.host == null) { 64 | executor.sendMessage("未设置默认服务器。") 65 | executor.sendMessage(getHelp()) 66 | } else { 67 | try { 68 | withTimeout(minecraftConfig.timeoutMillis) { status(executor, minecraftConfig.default.host!!, minecraftConfig.default.port) } 69 | } catch (e: TimeoutCancellationException) { 70 | executor.sendMessage("服务器连接超时") 71 | } catch (e: Exception) { 72 | executor.sendMessage("连接失败: " + e.localizedMessage) 73 | } 74 | } 75 | } 76 | } catch (e: TimeoutCancellationException) { 77 | executor.sendMessage("请求超时,请重试。") 78 | } 79 | } 80 | 81 | private fun mapMessageToRaw(message: Component): String { 82 | return if (message is TextComponent) message.content() 83 | else { 84 | buildString { message.children().map(::mapMessageToRaw).forEach(::append) } 85 | } 86 | } 87 | 88 | suspend fun status(executor: CommandExecutor, host: String, port: Int) = coroutineScope { 89 | val client = TcpClientSession(host, port, statusProtocol) 90 | 91 | val infoAsync = async(Dispatchers.IO) { 92 | suspendCancellableCoroutine { 93 | client.setFlag( 94 | MinecraftConstants.SERVER_INFO_HANDLER_KEY, 95 | ServerInfoHandler { _, i -> 96 | it.resumeWith(Result.success(i)) 97 | }) 98 | } 99 | } 100 | 101 | val pingTimeAsync = async(Dispatchers.IO) { 102 | suspendCancellableCoroutine { 103 | client.setFlag( 104 | MinecraftConstants.SERVER_PING_TIME_HANDLER_KEY, 105 | ServerPingTimeHandler { _, p -> 106 | it.resumeWith(Result.success(p)) 107 | }) 108 | } 109 | } 110 | 111 | client.connect(true) 112 | 113 | val (info, pingTime) = withTimeout(1000) { infoAsync.await() to pingTimeAsync.await() } 114 | 115 | val msg = buildString { 116 | appendLine("服务器延迟为 ${pingTime}ms") 117 | append(mapMessageToRaw(info.description).replace(Regex("§[\\w\\d]"), "")) 118 | appendLine( 119 | ", ${ 120 | info.versionInfo.versionName.replace( 121 | Regex("((thermos|cauldron|craftbukkit|mcpc|kcauldron|fml),?)+"), 122 | "" 123 | ) 124 | }" 125 | ) 126 | appendLine("玩家: ${info.playerInfo.onlinePlayers} / ${info.playerInfo.maxPlayers}") 127 | if (info.playerInfo.onlinePlayers > 0) { 128 | appendLine(info.playerInfo.players.joinToString(", ") { it.name }) 129 | } 130 | }.trim() 131 | 132 | if (executor is UserCommandExecutor) { 133 | info.iconPng?.let { executor.sendImage(it) } 134 | } 135 | executor.sendMessage(msg) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/romajitable/RomajiConverter.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.romajitable 2 | 3 | class RomajiConverter { 4 | } 5 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/romajitable/RomajiTable.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.romajitable; 2 | 3 | import com.squareup.moshi.JsonClass 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.Types 6 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 7 | 8 | @JsonClass(generateAdapter = true) 9 | data class Romaji(val hiragana: String, val katakana: String) 10 | 11 | object RomajiTable { 12 | val romajiTable: Map 13 | val katakanaTable: Map 14 | val maxLen: Int 15 | 16 | init { 17 | val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() 18 | romajiTable = 19 | moshi 20 | .adapter>(Types.newParameterizedType(Map::class.java, String::class.java, Romaji::class.java)) 21 | .fromJson(Romaji::class.java.getResource("/Romaji.json")!!.readText())!! 22 | 23 | maxLen = romajiTable.keys.maxOf { it.length } 24 | 25 | katakanaTable = 26 | moshi 27 | .adapter>(Types.newParameterizedType(Map::class.java, String::class.java, String::class.java)) 28 | .fromJson(Romaji::class.java.getResource("/Katakana.json")!!.readText())!! 29 | } 30 | 31 | fun romajiConvert(text: String): String { 32 | val romaji = text.lowercase() 33 | val katakana = StringBuilder() 34 | var pos = 0 35 | while (pos < romaji.length) { 36 | var length = maxLen 37 | if (romaji.length - pos < length) 38 | length = romaji.length - pos 39 | 40 | var found = false 41 | 42 | while (length > 0 && !found) { 43 | var lol_str = try { romaji.substring(pos, pos + length) } catch (e: IndexOutOfBoundsException) { null } 44 | val obj = lol_str?.let { romajiTable[it] } 45 | if (obj != null) { 46 | katakana.append(obj.katakana) 47 | pos += length 48 | found = true 49 | } 50 | length -= 1 51 | } 52 | 53 | if (!found) { 54 | katakana.append(romaji[pos]) 55 | pos += 1 56 | } 57 | } 58 | 59 | return katakana.toString() 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/timer/TimerCommand.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.timer 2 | 3 | import net.origind.destinybot.api.command.* 4 | import net.origind.destinybot.api.timer.LOCAL_DATE_TIME_FORMATTER 5 | import net.origind.destinybot.api.timer.TimerManager 6 | import java.time.Duration 7 | 8 | object TimerCommand : AbstractCommand("/cron") { 9 | init { 10 | arguments += ArgumentContext("task", StringArgument, true) 11 | arguments += ArgumentContext("interval", LongArgument, true) 12 | } 13 | 14 | override suspend fun execute(argument: ArgumentContainer, executor: CommandExecutor, context: CommandContext) { 15 | if (argument.hasArgument("task")) { 16 | if (!argument.hasArgument("interval")) 17 | throw ArgumentParseException("需要指定时长(ms),0为关闭。") 18 | 19 | val task = argument.getArgument("task") 20 | val interval = argument.getArgument("interval") 21 | 22 | if (!TimerManager.tasks.containsKey(task)) 23 | throw ArgumentParseException("不存在该计划任务。") 24 | 25 | if (interval <= 0L) { 26 | if (interval == 0L) { 27 | TimerManager.disable(task) 28 | executor.sendMessage("任务已关闭。") 29 | return 30 | } else { 31 | throw ArgumentParseException("你比0还小是要闹那样?") 32 | } 33 | } 34 | 35 | TimerManager.tasks[task]?.interval = Duration.ofMillis(interval) 36 | executor.sendMessage("任务已设置为每 ${interval}ms 执行一次。") 37 | } else { 38 | executor.sendMessage(buildString { 39 | appendLine("所有计划任务:") 40 | for ((name, task) in TimerManager.tasks) { 41 | append(name) 42 | append(" - ") 43 | append(task.interval.toMillis()) 44 | append("ms,") 45 | append("上次执行时间:") 46 | appendLine(LOCAL_DATE_TIME_FORMATTER.format(task.lastExecuted)) 47 | } 48 | }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/yahtzee/Dice.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.yahtzee 2 | 3 | import kotlin.random.Random 4 | 5 | data class Dice(var value: Int = 0, var fixed: Boolean = false) : Comparable { 6 | init { 7 | roll() 8 | } 9 | 10 | fun roll() { 11 | if (!fixed) 12 | value = Random.nextInt(1, 7) 13 | } 14 | 15 | override fun compareTo(other: Dice): Int { 16 | return value.compareTo(other.value) 17 | } 18 | 19 | override fun toString() = value.toString() 20 | } 21 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/yahtzee/GameStage.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.yahtzee 2 | 3 | enum class GameStage { 4 | MATCHMAKING, PLAYING 5 | } 6 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/yahtzee/YahtzeeGame.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.yahtzee 2 | 3 | 4 | class YahtzeeGame() { 5 | val players = mutableListOf() 6 | val stage = GameStage.MATCHMAKING 7 | 8 | suspend fun start() { 9 | if (players.size <= 1) { 10 | return 11 | } 12 | sendDices() 13 | } 14 | 15 | suspend fun sendDices() { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/yahtzee/YahtzeeGameManager.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.yahtzee 2 | 3 | object YahtzeeGameManager { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /destinybot-features/src/main/kotlin/net/origind/destinybot/features/yahtzee/YahtzeePlayer.kt: -------------------------------------------------------------------------------- 1 | package net.origind.destinybot.features.yahtzee 2 | 3 | class YahtzeePlayer(val qq: Long) { 4 | val dices = Array(6) { Dice() } 5 | } 6 | -------------------------------------------------------------------------------- /destinybot-features/src/main/resources/META-INF/services/net.origind.destinybot.api.plugin.Plugin: -------------------------------------------------------------------------------- 1 | net.origind.destinybot.features.FeaturesPlugin 2 | -------------------------------------------------------------------------------- /destinybot-features/src/test/kotlin/DestinyBotTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.squareup.moshi.Types 3 | import kotlinx.coroutines.asCoroutineDispatcher 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.runBlocking 6 | import net.origind.destinybot.features.bilibili.* 7 | import net.origind.destinybot.features.destiny.bungieUserToDestinyUser 8 | import net.origind.destinybot.features.destiny.image.getImage 9 | import net.origind.destinybot.features.destiny.image.toByteArray 10 | import net.origind.destinybot.features.getBodyAsync 11 | import net.origind.destinybot.features.github.CommitInfo 12 | import net.origind.destinybot.features.injdk.InjdkCommand 13 | import net.origind.destinybot.features.moshi 14 | import org.junit.jupiter.api.Assertions.assertFalse 15 | import org.junit.jupiter.api.Assertions.assertNotNull 16 | import org.junit.jupiter.api.Test 17 | import java.util.concurrent.Executors 18 | 19 | 20 | class DestinyBotTest { 21 | @Test 22 | fun testGitHub() { 23 | runBlocking { 24 | val type = Types.newParameterizedType(List::class.java, CommitInfo::class.java) 25 | val infos = moshi.adapter>(type).fromJson( 26 | getBodyAsync("https://api.github.com/repos/TRKS-Team/WFBot/commits?per_page=1").await())!! 27 | assertFalse(infos.isEmpty()) 28 | } 29 | } 30 | 31 | val COOKIE = """""" 32 | 33 | @Test 34 | fun testBilibili() { 35 | runBlocking { 36 | val id = searchUser("SourceForge").first().mid 37 | println(getUserInfo(15480779)) 38 | println(sameFollow(COOKIE, 15480779)) 39 | } 40 | } 41 | 42 | @Test 43 | fun testVdb() { 44 | runBlocking { 45 | VdbAPI.updateList() 46 | val pool = Executors.newFixedThreadPool(16) 47 | val executor = pool.asCoroutineDispatcher() 48 | 49 | val list = VdbAPI.vTuberList.vtbs.asSequence().flatMap { it.accounts }.filter { it.platform == "bilibili" } 50 | .drop(1440) 51 | .forEach { 52 | runBlocking { 53 | val response = follow("", 54 | COOKIE,it.id) 55 | if (response.code == 0) { 56 | println("Followed ${it.id}") 57 | } else if (response.code == 22013) { 58 | println(response.message) 59 | } 60 | else { 61 | throw Exception("Cannot follow ${it.id}: " + response) 62 | } 63 | delay(100) 64 | } 65 | } 66 | } 67 | } 68 | 69 | @Test 70 | fun testBungie() { 71 | runBlocking { 72 | println(bungieUserToDestinyUser("Strelizia", 5916)) 73 | } 74 | } 75 | 76 | @Test 77 | fun testWeeklyReport() { 78 | runBlocking { 79 | println(getLatestWeeklyReportURL()) 80 | assertNotNull(getImage("https:${getLatestWeeklyReportURL()}").toByteArray()) 81 | } 82 | } 83 | 84 | @Test 85 | fun testInjdk() { 86 | runBlocking { 87 | InjdkCommand.reload() 88 | println(InjdkCommand.jreDistro) 89 | println(InjdkCommand.jdkDistro) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /destinybot-features/src/test/kotlin/TimerTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import kotlinx.coroutines.* 3 | import org.junit.jupiter.api.Test 4 | import java.util.concurrent.Executors 5 | 6 | class TimerTest { 7 | @Test 8 | fun testTimedTask() { 9 | val runner = Executors.newFixedThreadPool(8).asCoroutineDispatcher() 10 | val scope = CoroutineScope(runner) 11 | scope.launch { 12 | while(true) { 13 | println("Task 1") 14 | delay(500) 15 | } 16 | } 17 | scope.launch { 18 | while(true) { 19 | println("Task 2") 20 | delay(2000) 21 | } 22 | } 23 | 24 | runBlocking { 25 | delay(6000) 26 | scope.cancel() 27 | println("Tasks cancelled") 28 | delay(6000) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /destinybot-manifest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "destinybot-manifest" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | tokio = { version = "1", features = ["full"] } 10 | mongodb = "2" 11 | bson = "2" 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | futures = "0.3" -------------------------------------------------------------------------------- /destinybot-manifest/README.md: -------------------------------------------------------------------------------- 1 | # Destiny Bot Manifest 2 | 3 | 一个 Rust 小程序,需要把 `d2-chs.json` 和 `d2-eng.json` 放到目录下。 -------------------------------------------------------------------------------- /destinybot-manifest/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use mongodb::{Client, options::ClientOptions, Database}; 4 | use bson::{Bson, Document}; 5 | 6 | #[tokio::main] 7 | async fn main() -> mongodb::error::Result<()> { 8 | let mut client_options = ClientOptions::parse("mongodb://localhost:27017").await?; 9 | client_options.app_name = Some("Destiny 2 Bot".to_string()); 10 | let client = Client::with_options(client_options)?; 11 | let db = client.database("destiny2"); 12 | db.drop(None).await?; 13 | read_and_add("d2-chs.json", "chs", &db).await.expect("IO error"); 14 | read_and_add("d2-eng.json", "eng", &db).await.expect("IO error"); 15 | Ok(()) 16 | } 17 | 18 | async fn read_and_add>(path: P, language: &'static str, database: &Database) -> std::io::Result<()> { 19 | let file = tokio::fs::read_to_string(path).await?; 20 | let file_replaced = file.replace(r#"BungieNet.Engine.Contract.Destiny.World.Definitions.IDestinyDisplayDefinition.displayProperties"#, "bungieDisplayProperties"); 21 | 22 | let bson = serde_json::from_str::(&file_replaced).expect("Cannot parse JSON to BSON"); 23 | let tasks = bson.into_iter().map(|(definition, bson)| { 24 | let database = database.clone(); 25 | tokio::spawn(async move { 26 | let collection = format!("{}_{}", &definition, language); 27 | let collection = database.collection::(&collection); 28 | 29 | if let Bson::Document(doc) = bson { 30 | let docs = doc.into_iter().map(|(id, entry)| { 31 | if let Bson::Document(d) = entry { 32 | let mut d = d; 33 | d.insert("_id", id.clone()); 34 | Some(d) 35 | } else { 36 | None 37 | } 38 | }).map(|x| x.unwrap()).collect::>(); 39 | if !docs.is_empty() { 40 | collection.insert_many(docs, None).await.unwrap(); 41 | } 42 | } 43 | }) 44 | }); 45 | futures::future::join_all(tasks).await; 46 | Ok(()) 47 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | miraiVersion=2.14.0 3 | ktorVersion=2.1.0 4 | kotlin.incremental.multiplatform=true 5 | kotlin.parallel.tasks.in.project=true 6 | 7 | api.version=1.1.0 8 | core.version=1.1.0 9 | features.version=1.1.0 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectUranus/destiny-bot-mirai/d06fdc4e35a3991a14a9ee69b9f8ddd5ba5def3e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | ## update_manifest.js 4 | 5 | Download https://www.bungie.net/Platform/Destiny2/Manifest/ and get chs eng json -------------------------------------------------------------------------------- /scripts/update_manifest.js: -------------------------------------------------------------------------------- 1 | // & 'C:\Program Files\MongoDB\Server\4.1\bin\mongodump.exe' /gzip /archive:destiny2.gz /db:destiny2 2 | // mongorestore --gzip --archive=destiny2.gz 3 | const fs = require('fs'); 4 | 5 | const MongoClient = require('mongodb').MongoClient; 6 | const url = 'mongodb://localhost:27017/'; 7 | 8 | MongoClient.connect( 9 | url, 10 | { useNewUrlParser: true, useUnifiedTopology: true }, 11 | function (err, db) { 12 | if (err) throw err; 13 | db.db('destiny2').dropDatabase(); 14 | const dbo = db.db('destiny2'); 15 | 16 | let d2obj = JSON.parse( 17 | fs 18 | .readFileSync('d2-chs.json') 19 | .toString() 20 | .replace( 21 | /BungieNet\.Engine\.Contract\.Destiny\.World\.Definitions\.IDestinyDisplayDefinition\.displayProperties/g, 22 | '' 23 | ) 24 | ); 25 | 26 | for (const definition in d2obj) { 27 | for (const id in d2obj[definition]) { 28 | d2obj[definition][id]['_id'] = id; 29 | dbo.collection(definition + '_chs').insertOne( 30 | d2obj[definition][id], 31 | function (err, res) { 32 | if (err) throw err; 33 | } 34 | ); 35 | } 36 | } 37 | 38 | d2obj = JSON.parse( 39 | fs 40 | .readFileSync('d2-eng.json') 41 | .toString() 42 | .replace( 43 | /BungieNet\.Engine\.Contract\.Destiny\.World\.Definitions\.IDestinyDisplayDefinition\.displayProperties/g, 44 | '' 45 | ) 46 | ); 47 | 48 | for (const definition in d2obj) { 49 | for (const id in d2obj[definition]) { 50 | d2obj[definition][id]['_id'] = id; 51 | dbo.collection(definition + '_eng').insertOne( 52 | d2obj[definition][id], 53 | function (err, res) { 54 | if (err) throw err; 55 | } 56 | ); 57 | } 58 | } 59 | } 60 | ); 61 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'destinybot' 2 | 3 | include 'destinybot-api', 'destinybot-core', 'destinybot-features' 4 | --------------------------------------------------------------------------------