├── .editorconfig ├── .github ├── WhereIsTheGreenButton.png └── workflows │ ├── Gradle CI.yml │ ├── PROD OK Build.yml │ └── Release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs └── img │ ├── cookie.png │ ├── demo1.png │ ├── demo2.png │ ├── demo3.png │ ├── dynamic-bot.png │ └── help.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── top │ │ └── colter │ │ └── mirai │ │ └── plugin │ │ └── bilibili │ │ ├── BiliBiliDynamic.kt │ │ ├── BiliConfig.kt │ │ ├── BiliData.kt │ │ ├── Init.kt │ │ ├── api │ │ ├── Api.kt │ │ ├── Dynamic.kt │ │ ├── General.kt │ │ ├── Live.kt │ │ ├── Pgc.kt │ │ └── User.kt │ │ ├── client │ │ └── BiliClient.kt │ │ ├── command │ │ ├── DynamicCommand.kt │ │ ├── GroupOrContact.kt │ │ └── GroupOrContactParser.kt │ │ ├── data │ │ ├── Article.kt │ │ ├── BascLink.kt │ │ ├── BiliCookie.kt │ │ ├── BiliDetail.kt │ │ ├── BiliMessage.kt │ │ ├── BiliSearch.kt │ │ ├── BiliUser.kt │ │ ├── Dynamic.kt │ │ ├── DynamicImageQuality.kt │ │ ├── DynamicImageTheme.kt │ │ ├── Follow.kt │ │ ├── General.kt │ │ ├── Live.kt │ │ ├── Login.kt │ │ ├── Pgc.kt │ │ ├── Result.kt │ │ ├── Video.kt │ │ └── Vote.kt │ │ ├── draw │ │ ├── DynamicDraw.kt │ │ ├── DynamicMajorDraw.kt │ │ ├── DynamicModuleDraw.kt │ │ ├── General.kt │ │ ├── LiveDraw.kt │ │ └── QrCodeDraw.kt │ │ ├── old │ │ ├── BiliPluginConfig.kt │ │ ├── BiliSubscribeData.kt │ │ └── DataMigration.kt │ │ ├── service │ │ ├── AtAllService.kt │ │ ├── ConfigService.kt │ │ ├── DynamicService.kt │ │ ├── FilterService.kt │ │ ├── General.kt │ │ ├── GroupService.kt │ │ ├── LoginService.kt │ │ ├── PgcService.kt │ │ ├── ResolveLinkService.kt │ │ └── TemplateService.kt │ │ ├── tasker │ │ ├── BiliCheckTasker.kt │ │ ├── BiliTasker.kt │ │ ├── CacheClearTasker.kt │ │ ├── DynamicCheckTasker.kt │ │ ├── DynamicMessageTasker.kt │ │ ├── ListenerTasker.kt │ │ ├── LiveCheckTasker.kt │ │ ├── LiveCloseCheckTasker.kt │ │ ├── LiveMessageTasker.kt │ │ └── SendTasker.kt │ │ └── utils │ │ ├── FontUtils.kt │ │ ├── General.kt │ │ ├── Json2DataClass.kt │ │ ├── JsonUtils.kt │ │ └── translate │ │ ├── HttpGet.kt │ │ ├── MD5.kt │ │ ├── TransApi.kt │ │ └── TransResult.kt └── resources │ ├── META-INF │ └── services │ │ └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin │ ├── font │ └── FansCard.ttf │ ├── icon │ ├── BILIBILI_LOGO.svg │ ├── DISPUTE.svg │ ├── FORWARD.svg │ ├── LIVE.svg │ ├── ORGANIZATION_OFFICIAL_VERIFY.svg │ ├── PERSONAL_OFFICIAL_VERIFY.svg │ ├── RICH_TEXT_NODE_TYPE_BV.svg │ ├── RICH_TEXT_NODE_TYPE_LOTTERY.svg │ ├── RICH_TEXT_NODE_TYPE_VOTE.svg │ ├── RICH_TEXT_NODE_TYPE_WEB.svg │ └── TOPIC.svg │ └── image │ ├── Blocked_BG_Day.png │ ├── HELP.png │ └── IMAGE_MISS.png └── test ├── kotlin ├── DrawDynamicTest.kt └── PluginTest.kt └── resources └── json └── dynamic_item ├── 1029210402684141574.json ├── 1031201757795975177.json ├── 767166448722247682.json └── MASKED_sponsor_only_unlocked.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt, kts}] 2 | max_line_length = 120 3 | tab_width = 4 4 | ij_continuation_indent_size = 4 5 | indent_size = 4 -------------------------------------------------------------------------------- /.github/WhereIsTheGreenButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/.github/WhereIsTheGreenButton.png -------------------------------------------------------------------------------- /.github/workflows/Gradle CI.yml: -------------------------------------------------------------------------------- 1 | name: Gradle CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push and pull request events but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | # Allows to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | jobs: 17 | 18 | build: 19 | 20 | name: Gradle-Build 21 | 22 | # The type of runner that the job will run on 23 | runs-on: ubuntu-20.04 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | 31 | # Setup JDK 32 | - name: Setup Java JDK 33 | uses: actions/setup-java@v1.4.3 34 | with: 35 | java-version: 11 36 | 37 | # Validate Gradle Wrapper 38 | - name: Gradle Wrapper Validation 39 | uses: gradle/wrapper-validation-action@v1.0.3 40 | 41 | # Build 42 | - name: Make gradlew executable 43 | run: chmod +x ./gradlew 44 | - name: Build with Gradle 45 | run: ./gradlew build 46 | 47 | # Upload File 48 | 49 | - name: Upload All Build File 50 | uses: actions/upload-artifact@v2 51 | with: 52 | name: All File 53 | path: build 54 | -------------------------------------------------------------------------------- /.github/workflows/PROD OK Build.yml: -------------------------------------------------------------------------------- 1 | name: PROD OK Build 2 | 3 | on: 4 | 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | 14 | build: 15 | 16 | name: Jar Build 17 | 18 | runs-on: ubuntu-20.04 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup Java JDK 25 | uses: actions/setup-java@v1.4.3 26 | with: 27 | java-version: 11 28 | 29 | - name: Gradle Wrapper Validation 30 | uses: gradle/wrapper-validation-action@v1.0.3 31 | 32 | - name: Make gradlew executable 33 | run: chmod +x ./gradlew 34 | - name: Build with Gradle 35 | run: ./gradlew buildPlugin 36 | 37 | # Upload File 38 | - name: Upload Jar File 39 | uses: actions/upload-artifact@v2 40 | with: 41 | name: Jar File 42 | path: build/mirai 43 | 44 | - name: Upload All Build File 45 | uses: actions/upload-artifact@v2 46 | with: 47 | name: All File 48 | path: build 49 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Github 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | 10 | build: 11 | 12 | name: Build and Release 13 | 14 | runs-on: ubuntu-20.04 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Setup Java JDK 21 | uses: actions/setup-java@v1.4.3 22 | with: 23 | java-version: 11 24 | 25 | - name: Gradle Wrapper Validation 26 | uses: gradle/wrapper-validation-action@v1.0.3 27 | 28 | - name: Make gradlew executable 29 | run: chmod +x ./gradlew 30 | 31 | - name: Build with Gradle 32 | run: ./gradlew buildPlugin 33 | 34 | - name: Upload Jar File 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: Jar File 38 | path: build/mirai 39 | 40 | - name: Upload All Build File 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: All File 44 | path: build 45 | 46 | - name: Release to Github 47 | uses: "marvinpinto/action-automatic-releases@latest" 48 | with: 49 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 50 | automatic_release_tag: ${{steps.branch_name.outputs.SOURCE_TAG}} 51 | prerelease: false 52 | title: ${{steps.branch_name.outputs.SOURCE_TAG}} 53 | files: | 54 | build/mirai/*.jar 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | #Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | .run/ 117 | 118 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 119 | !gradle-wrapper.jar 120 | 121 | 122 | # Local Test Launch point 123 | src/test/kotlin/RunTerminal.kt 124 | 125 | # Mirai console files with direct bootstrap 126 | /debug-sandbox/config 127 | /debug-sandbox/data 128 | /debug-sandbox/plugins 129 | /debug-sandbox/bots 130 | 131 | # Local Test Launch Point working directory 132 | /debug-sandbox 133 | 134 | /config 135 | /data 136 | /logs 137 | /plugins 138 | /bin 139 | /plugin-libraries 140 | /plugin-shared-libraries 141 | 142 | # ignore test outputs 143 | src/test/resources/output/** -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | val kotlinVersion = "2.0.0" 3 | kotlin("jvm") version kotlinVersion 4 | kotlin("plugin.serialization") version kotlinVersion 5 | 6 | id("net.mamoe.mirai-console") version "2.15.0" 7 | id("me.him188.maven-central-publish") version "1.0.0-dev-3" 8 | } 9 | 10 | group = "top.colter" 11 | version = "3.2.14" 12 | 13 | repositories { 14 | mavenLocal() 15 | mavenCentral() 16 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 17 | } 18 | 19 | mavenCentralPublish { 20 | useCentralS01() 21 | singleDevGithubProject("Colter23", "bilibili-dynamic-mirai-plugin") 22 | licenseFromGitHubProject("AGPL-3.0", "master") 23 | publication { 24 | artifact(tasks.getByName("buildPlugin")) 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation("io.ktor:ktor-client-okhttp:3.0.3") { 30 | exclude(group = "org.jetbrains.kotlin") 31 | exclude(group = "org.jetbrains.kotlinx") 32 | exclude(group = "org.slf4j") 33 | } 34 | implementation("io.ktor:ktor-client-encoding:3.0.3") { 35 | exclude(group = "org.jetbrains.kotlin") 36 | exclude(group = "org.jetbrains.kotlinx") 37 | exclude(group = "org.slf4j") 38 | } 39 | implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3") { 40 | exclude(group = "org.jetbrains.kotlin") 41 | exclude(group = "org.jetbrains.kotlinx") 42 | exclude(group = "org.slf4j") 43 | } 44 | 45 | implementation("com.google.zxing:javase:3.5.0") 46 | compileOnly("xyz.cssxsh.mirai:mirai-skia-plugin:1.3.1") 47 | 48 | testImplementation(kotlin("test", "1.7.0")) 49 | testImplementation("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:0.7.27") 50 | testImplementation("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:0.7.27") 51 | testImplementation("org.jetbrains.skiko:skiko-awt-runtime-linux-arm64:0.7.27") 52 | testImplementation("org.jetbrains.skiko:skiko-awt-runtime-macos-x64:0.7.27") 53 | testImplementation("org.jetbrains.skiko:skiko-awt-runtime-macos-arm64:0.7.27") 54 | } 55 | 56 | mirai { 57 | jvmTarget = JavaVersion.VERSION_11 58 | } -------------------------------------------------------------------------------- /docs/img/cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/docs/img/cookie.png -------------------------------------------------------------------------------- /docs/img/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/docs/img/demo1.png -------------------------------------------------------------------------------- /docs/img/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/docs/img/demo2.png -------------------------------------------------------------------------------- /docs/img/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/docs/img/demo3.png -------------------------------------------------------------------------------- /docs/img/dynamic-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/docs/img/dynamic-bot.png -------------------------------------------------------------------------------- /docs/img/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/docs/img/help.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/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.6.2-all.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 | 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 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | jcenter() 7 | maven("https://dl.bintray.com/kotlin/kotlin-eap") 8 | } 9 | } 10 | rootProject.name = "bilibili-dynamic-mirai-plugin" -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/BiliBiliDynamic.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili 2 | 3 | import kotlinx.coroutines.channels.Channel 4 | import kotlinx.coroutines.launch 5 | import net.mamoe.mirai.console.MiraiConsole 6 | import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register 7 | import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister 8 | import net.mamoe.mirai.console.extension.PluginComponentStorage 9 | import net.mamoe.mirai.console.permission.PermissionId 10 | import net.mamoe.mirai.console.permission.PermissionService 11 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription 12 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin 13 | import net.mamoe.mirai.console.plugin.name 14 | import net.mamoe.mirai.console.plugin.version 15 | import net.mamoe.mirai.console.util.SemVersion 16 | import net.mamoe.mirai.utils.info 17 | import top.colter.mirai.plugin.bilibili.command.DynamicCommand 18 | import top.colter.mirai.plugin.bilibili.data.* 19 | import top.colter.mirai.plugin.bilibili.old.migration 20 | import top.colter.mirai.plugin.bilibili.old.updateData 21 | import top.colter.mirai.plugin.bilibili.tasker.* 22 | 23 | object BiliBiliDynamic : KotlinPlugin( 24 | JvmPluginDescription( 25 | id = "top.colter.bilibili-dynamic-mirai-plugin", 26 | name = "BiliBili Dynamic", 27 | version = "3.2.14", 28 | ) { 29 | author("Colter") 30 | dependsOn("xyz.cssxsh.mirai.plugin.mirai-skia-plugin", ">= 1.1.0") 31 | } 32 | ) { 33 | 34 | var uid: Long = 0L 35 | var tagid: Int = 0 36 | 37 | var cookie = BiliCookie() 38 | 39 | val dynamicChannel = Channel(20) 40 | val liveChannel = Channel(20) 41 | val messageChannel = Channel(20) 42 | val missChannel = Channel(10) 43 | 44 | val liveUsers = mutableMapOf() 45 | 46 | val liveGwp = PermissionId(BiliBiliDynamic.description.id, "live.atall") 47 | val videoGwp = PermissionId(BiliBiliDynamic.description.id, "video.atall") 48 | val crossContact = PermissionId(BiliBiliDynamic.description.id, "crossContact") 49 | 50 | override fun PluginComponentStorage.onLoad() { 51 | /** 52 | * run after auto login 53 | * @author cssxsh 54 | */ 55 | runAfterStartup { 56 | updateData() 57 | 58 | DynamicCheckTasker.start() 59 | LiveCheckTasker.start() 60 | DynamicMessageTasker.start() 61 | LiveMessageTasker.start() 62 | SendTasker.start() 63 | ListenerTasker.start() 64 | if (BiliConfig.enableConfig.liveCloseNotifyEnable) LiveCloseCheckTasker.start() 65 | if (BiliConfig.enableConfig.cacheClearEnable) CacheClearTasker.start() 66 | } 67 | } 68 | 69 | override fun onEnable() { 70 | // XXX: mirai console version check 71 | check(SemVersion.parseRangeRequirement(">= 2.12.0-RC").test(MiraiConsole.version)) { 72 | "$name $version 需要 Mirai-Console 版本 >= 2.12.0,目前版本是 ${MiraiConsole.version}" 73 | } 74 | logger.info { "BiliBili Dynamic Plugin loaded" } 75 | 76 | PermissionService.INSTANCE.register(liveGwp, "直播At全体") 77 | PermissionService.INSTANCE.register(videoGwp, "视频At全体") 78 | PermissionService.INSTANCE.register(crossContact, "跨聊天语境控制") 79 | 80 | DynamicCommand.register() 81 | 82 | BiliData.reload() 83 | BiliConfig.reload() 84 | BiliImageTheme.reload() 85 | BiliImageQuality.reload() 86 | 87 | migration() 88 | 89 | launch { initData() } 90 | } 91 | 92 | override fun onDisable() { 93 | DynamicCommand.unregister() 94 | dynamicChannel.close() 95 | messageChannel.close() 96 | 97 | BiliTasker.cancelAll() 98 | 99 | BiliData.save() 100 | BiliConfig.save() 101 | } 102 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/BiliData.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili 2 | 3 | import kotlinx.serialization.Serializable 4 | import net.mamoe.mirai.console.data.AutoSavePluginData 5 | import net.mamoe.mirai.console.data.ValueDescription 6 | import net.mamoe.mirai.console.data.value 7 | import top.colter.mirai.plugin.bilibili.utils.findContact 8 | import top.colter.mirai.plugin.bilibili.utils.findContactAll 9 | import top.colter.mirai.plugin.bilibili.utils.name 10 | import java.time.Instant 11 | 12 | object BiliData : AutoSavePluginData("BiliData") { 13 | @ValueDescription("数据版本") 14 | var dataVersion: Int by value(0) 15 | 16 | // key: uid 17 | @ValueDescription("订阅信息") 18 | val dynamic: MutableMap by value(mutableMapOf(0L to SubData("ALL"))) 19 | 20 | // key: contact 21 | @ValueDescription("动态过滤") 22 | val filter: MutableMap> by value() 23 | 24 | // key: template name 25 | @ValueDescription("动态推送模板") 26 | val dynamicPushTemplate: MutableMap> by value() 27 | 28 | // key: template name 29 | @ValueDescription("直播推送模板") 30 | val livePushTemplate: MutableMap> by value() 31 | 32 | // key: template name 33 | @ValueDescription("直播结束模板") 34 | val liveCloseTemplate: MutableMap> by value() 35 | 36 | // key: contact 37 | @ValueDescription("AtAll") 38 | val atAll: MutableMap>> by value() 39 | 40 | // key: group name 41 | @ValueDescription("分组") 42 | val group: MutableMap by value() 43 | 44 | // key: season id 45 | @ValueDescription("番剧") 46 | val bangumi: MutableMap by value(mutableMapOf()) 47 | } 48 | 49 | @Serializable 50 | data class SubData( 51 | val name: String, 52 | var color: String? = null, 53 | var last: Long = Instant.now().epochSecond, 54 | var lastLive: Long = Instant.now().epochSecond, 55 | val contacts: MutableSet = mutableSetOf(), 56 | val banList: MutableMap = mutableMapOf(), 57 | ) 58 | 59 | @Serializable 60 | data class Group( 61 | val name: String, 62 | val creator: Long, 63 | val admin: MutableSet = mutableSetOf(), 64 | val contacts: MutableSet = mutableSetOf(), 65 | ) { 66 | override fun toString(): String { 67 | return """ 68 | 分组名: $name 69 | 创建者: ${findContactAll(creator)?.run { "$name($id)" }?:creator} 70 | 71 | 管理员: 72 | ${admin.joinToString("\n") { findContactAll(it)?.run { "$name($id)" }?:it.toString() }.ifEmpty { "暂无管理员" }} 73 | 74 | 用户: 75 | ${contacts.joinToString("\n") { findContact(it)?.run { "$name($id)" }?:it }.ifEmpty { "暂无用户" }} 76 | """.trimIndent() 77 | } 78 | } 79 | 80 | @Serializable 81 | data class Bangumi( 82 | val title: String, 83 | val seasonId: Long, 84 | val mediaId: Long, 85 | val type: String, 86 | var isEnd: Boolean = false, 87 | var color: String? = null, 88 | val contacts: MutableSet = mutableSetOf(), 89 | ) 90 | 91 | @Serializable 92 | enum class FilterType { 93 | TYPE, 94 | REGULAR 95 | } 96 | 97 | @Serializable 98 | data class DynamicFilter( 99 | val typeSelect: TypeFilter = TypeFilter(), 100 | val regularSelect: RegularFilter = RegularFilter(), 101 | ) 102 | 103 | @Serializable 104 | data class TypeFilter( 105 | var mode: FilterMode = FilterMode.BLACK_LIST, 106 | val list: MutableList = mutableListOf() 107 | ) 108 | 109 | @Serializable 110 | data class RegularFilter( 111 | var mode: FilterMode = FilterMode.BLACK_LIST, 112 | val list: MutableList = mutableListOf() 113 | ) 114 | 115 | @Serializable 116 | enum class FilterMode(val value: String) { 117 | WHITE_LIST("白名单"), 118 | BLACK_LIST("黑名单") 119 | } 120 | 121 | @Serializable 122 | enum class DynamicFilterType(val value: String) { 123 | DYNAMIC("动态"), 124 | FORWARD("转发动态"), 125 | VIDEO("视频"), 126 | MUSIC("音乐"), 127 | ARTICLE("专栏"), 128 | LIVE("直播"), 129 | } 130 | 131 | enum class AtAllType(val value: String) { 132 | ALL("全部"), 133 | DYNAMIC("全部动态"), 134 | VIDEO("视频"), 135 | MUSIC("音乐"), 136 | ARTICLE("专栏"), 137 | LIVE("直播"), 138 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/Init.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili 2 | 3 | import top.colter.mirai.plugin.bilibili.BiliConfig.accountConfig 4 | import top.colter.mirai.plugin.bilibili.api.createGroup 5 | import top.colter.mirai.plugin.bilibili.api.followGroup 6 | import top.colter.mirai.plugin.bilibili.api.userInfo 7 | import top.colter.mirai.plugin.bilibili.data.EditThisCookie 8 | import top.colter.mirai.plugin.bilibili.data.toCookie 9 | import top.colter.mirai.plugin.bilibili.utils.FontUtils.loadTypeface 10 | import top.colter.mirai.plugin.bilibili.utils.biliClient 11 | import top.colter.mirai.plugin.bilibili.utils.decode 12 | import xyz.cssxsh.mirai.skia.downloadTypeface 13 | import kotlin.io.path.createDirectory 14 | import kotlin.io.path.exists 15 | import kotlin.io.path.forEachDirectoryEntry 16 | import kotlin.io.path.name 17 | 18 | suspend fun initData() { 19 | checkCookie() 20 | initTagid() 21 | loadFonts() 22 | } 23 | 24 | suspend fun checkCookie() { 25 | val cookieFile = BiliBiliDynamic.dataFolder.resolve("cookies.json") 26 | if (cookieFile.exists()) { 27 | try { 28 | val cookie = cookieFile.readText().decode>().toCookie() 29 | if (!cookie.isEmpty()) { 30 | BiliBiliDynamic.cookie = cookie 31 | } else { 32 | BiliBiliDynamic.logger.error("cookies.json 中缺少必要的值 [SESSDATA] [bili_jct]") 33 | } 34 | } catch (e: Exception) { 35 | BiliBiliDynamic.logger.error("解析 cookies.json 失败") 36 | } 37 | } 38 | if (BiliBiliDynamic.cookie.isEmpty()) BiliBiliDynamic.cookie.parse(accountConfig.cookie) 39 | 40 | try { 41 | BiliBiliDynamic.uid = biliClient.userInfo()?.mid!! 42 | BiliBiliDynamic.logger.info("BiliBili UID: ${BiliBiliDynamic.uid}") 43 | } catch (e: Exception) { 44 | BiliBiliDynamic.logger.error(e.message) 45 | BiliBiliDynamic.logger.error("如未登录,请bot管理员在聊天环境内发送 /bili login 进行登录") 46 | return 47 | } 48 | } 49 | 50 | suspend fun initTagid() { 51 | if (accountConfig.autoFollow && accountConfig.followGroup.isNotEmpty()) { 52 | try { 53 | biliClient.followGroup()?.forEach { 54 | if (it.name == accountConfig.followGroup) { 55 | BiliBiliDynamic.tagid = it.tagId 56 | return 57 | } 58 | } 59 | val res = biliClient.createGroup(accountConfig.followGroup) ?: throw Exception() 60 | BiliBiliDynamic.tagid = res.tagId 61 | } catch (e: Exception) { 62 | BiliBiliDynamic.logger.error("初始化分组失败 ${e.message}") 63 | } 64 | 65 | } 66 | } 67 | 68 | suspend fun loadFonts() { 69 | val fontFolder = BiliBiliDynamic.dataFolder.resolve("font") 70 | val fontFolderPath = BiliBiliDynamic.dataFolderPath.resolve("font") 71 | val LXGW = fontFolder.resolve("LXGWWenKai-Bold.ttf") 72 | 73 | fontFolderPath.apply { 74 | if (!exists()) createDirectory() 75 | if (fontFolder.listFiles().none { it.isFile } || !LXGW.exists()) { 76 | try { 77 | downloadTypeface(fontFolder, "https://file.zfont.cn/d/file/font_cn_file/霞鹜文楷-v1.235.2.zip") 78 | val f = fontFolder.resolve("霞鹜文楷-v1.235.2") 79 | f.resolve("LXGWWenKai-Bold.ttf").copyTo(LXGW) 80 | try { 81 | f.walkBottomUp().onLeave { it.delete() } 82 | }catch (_: Exception) { } 83 | }catch (e: Throwable) { 84 | BiliBiliDynamic.logger.error("下载字体失败! $e") 85 | } 86 | } 87 | forEachDirectoryEntry { 88 | if (it.toFile().isFile) loadTypeface(it.toString(), it.name.split(".").first()) 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/api/Api.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.api 2 | 3 | // Login 4 | const val LOGIN_QRCODE = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate" 5 | const val LOGIN_INFO = "https://passport.bilibili.com/x/passport-login/web/qrcode/poll" 6 | 7 | // Dynamic 8 | const val NEW_DYNAMIC = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all" 9 | const val SPACE_DYNAMIC = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space" 10 | const val DYNAMIC_DETAIL = "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail" 11 | 12 | // Video 13 | const val VIDEO_DETAIL = "https://api.bilibili.com/x/web-interface/view" 14 | 15 | // Article 16 | const val ARTICLE_DETAIL = "https://api.bilibili.com/x/article/viewinfo" 17 | const val ARTICLE_LIST = "https://api.bilibili.com/x/article/cards" 18 | 19 | // Live 20 | const val LIVE_LIST = "https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/GetWebList" 21 | const val LIVE_STATUS_BATCH = "https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids" 22 | const val LIVE_DETAIL = "https://api.live.bilibili.com/room/v1/Room/get_info" 23 | 24 | // Search 25 | const val SEARCH = "https://api.bilibili.com/x/web-interface/search/type" 26 | 27 | // Space 28 | const val USER_INFO = "https://api.bilibili.com/x/space/acc/info" 29 | const val USER_INFO_WBI = "https://api.bilibili.com/x/space/wbi/acc/info" 30 | const val USER_ID = "https://api.bilibili.com/x/web-interface/nav" 31 | const val SPACE_SEARCH = "https://api.bilibili.com/x/space/wbi/arc/search" 32 | 33 | // Follow 34 | const val IS_FOLLOW = "https://api.bilibili.com/x/relation" 35 | const val FOLLOW = "https://api.bilibili.com/x/relation/modify" 36 | 37 | // Group 分组 38 | const val GROUP_LIST = "https://api.bilibili.com/x/relation/tags" 39 | const val CREATE_GROUP = "https://api.bilibili.com/x/relation/tag/create" 40 | const val DEL_FOLLOW_GROUP = "https://api.bilibili.com/x/relation/tag/del" 41 | const val ADD_USER = "https://api.bilibili.com/x/relation/tags/addUsers" 42 | 43 | // Pgc 番剧 44 | const val PGC_MEDIA_INFO = "https://api.bilibili.com/pgc/review/user" 45 | const val PGC_INFO = "https://api.bilibili.com/pgc/view/web/season" 46 | const val FOLLOW_PGC = "https://api.bilibili.com/pgc/web/follow/add" 47 | const val UNFOLLOW_PGC = "https://api.bilibili.com/pgc/web/follow/del" 48 | 49 | // Short Link 50 | const val SHORT_LINK = "https://api.bilibili.com/x/share/click" 51 | 52 | // Twemoji CDN 53 | const val TWEMOJI = "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72" 54 | 55 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/api/Dynamic.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.api 2 | 3 | import io.ktor.client.request.* 4 | import top.colter.mirai.plugin.bilibili.client.BiliClient 5 | import top.colter.mirai.plugin.bilibili.data.* 6 | 7 | /** 8 | * 获取账号全部最新动态 9 | * @param page 分页 (每页20左右) 10 | * @param type 动态类型 video: 视频 pgc: 番剧 article: 专栏 11 | */ 12 | suspend fun BiliClient.getNewDynamic(page: Int = 1, type: String = "all"): DynamicList? { 13 | return getData(NEW_DYNAMIC) { 14 | parameter("timezone_offset", "-480") 15 | parameter("type", type) 16 | parameter("page", page) 17 | parameter("features", "itemOpusStyle") 18 | } 19 | } 20 | 21 | /** 22 | * 获取用户最新动态 23 | * @param uid 用户ID 24 | * @param hasTop 是否包含置顶动态 25 | * @param offset 动态偏移 26 | */ 27 | suspend fun BiliClient.getUserNewDynamic(uid: Long, hasTop: Boolean = false, offset: String = ""): DynamicList? { 28 | return getData(if (hasTop) SPACE_DYNAMIC else NEW_DYNAMIC) { 29 | parameter("timezone_offset", "-480") 30 | parameter("host_mid", uid) 31 | parameter("offset", offset) 32 | parameter("features", "itemOpusStyle") 33 | } 34 | } 35 | 36 | /** 37 | * 获取指定动态详情 38 | * @param did 动态ID 39 | */ 40 | suspend fun BiliClient.getDynamicDetail(did: String): DynamicItem? { 41 | return getData(DYNAMIC_DETAIL) { 42 | parameter("timezone_offset", "-480") 43 | parameter("id", did) 44 | parameter("features", "itemOpusStyle") 45 | }?.item 46 | } 47 | 48 | suspend fun BiliClient.getVideoDetail(id: String): VideoDetail? { 49 | return getData(VIDEO_DETAIL) { 50 | if (id.contains("BV")) parameter("bvid", id) 51 | else parameter("aid", id.removePrefix("av")) 52 | } 53 | } 54 | 55 | suspend fun BiliClient.getArticleDetailOld(id: String): ArticleDetail? { 56 | return getData(ARTICLE_DETAIL) { 57 | if (id.startsWith("cv")) parameter("id", id.removePrefix("cv")) 58 | else parameter("id", id) 59 | } 60 | } 61 | 62 | suspend fun BiliClient.getArticleDetail(id: String): ArticleDetail? { 63 | return getArticleList(listOf(id))?.get(id) 64 | } 65 | 66 | suspend fun BiliClient.getArticleList(ids: List): Map? { 67 | return getData(ARTICLE_LIST) { 68 | parameter("ids", ids.joinToString(",")) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/api/General.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.api 2 | 3 | import io.ktor.client.call.* 4 | import io.ktor.client.request.* 5 | import io.ktor.http.* 6 | import top.colter.mirai.plugin.bilibili.client.BiliClient 7 | import top.colter.mirai.plugin.bilibili.data.BiliResult 8 | import top.colter.mirai.plugin.bilibili.data.ShortLinkData 9 | import top.colter.mirai.plugin.bilibili.data.WbiImg 10 | import top.colter.mirai.plugin.bilibili.utils.* 11 | import java.time.LocalDate 12 | 13 | fun twemoji(code: String) = "$TWEMOJI/$code.png" 14 | 15 | private var isLogin = true 16 | 17 | internal suspend inline fun BiliClient.getData( 18 | url: String, 19 | crossinline block: HttpRequestBuilder.() -> Unit = {} 20 | ): T? { 21 | val res = get(url, block) 22 | 23 | return if (res.code == -101) { 24 | if (isLogin) actionNotify("账号登录失效,请使用 /bili login 重新登录") 25 | isLogin = false 26 | throw Exception("账号登录失效,请使用 /bili login 重新登录") 27 | } else if (res.code != 0 || res.data == null) { 28 | throw Exception("URL: $url, CODE: ${res.code}, MSG: ${res.message}") 29 | } else { 30 | isLogin = true 31 | res.data.decode() 32 | } 33 | } 34 | internal suspend inline fun BiliClient.getDataWithWbi( 35 | url: String, 36 | crossinline block: HttpRequestBuilder.() -> Unit = {} 37 | ): T? { 38 | val builder = HttpRequestBuilder() 39 | builder.block() 40 | val params = builder.url.parameters.build().formUrlEncode() 41 | val wts = System.currentTimeMillis() / 1000 42 | val wrid = "$params&wts=$wts${getVerifyString()}".md5() 43 | return getData(url) { 44 | block() 45 | parameter("w_rid", wrid) 46 | parameter("wts", wts) 47 | } 48 | } 49 | 50 | suspend fun BiliClient.redirect(url: String): String? { 51 | return useHttpClient { 52 | it.config { 53 | followRedirects = false 54 | expectSuccess = false 55 | }.head(url) 56 | }.headers[HttpHeaders.Location] 57 | } 58 | 59 | suspend fun BiliClient.videoShortLink(aid: String): String? = 60 | toShortLink(aid, "main.ugc-video-detail.0.0.pv", "vinfo_player") 61 | 62 | suspend fun BiliClient.articleShortLink(aid: String): String? = 63 | toShortLink(aid, "read.column-detail.roof.8.click") 64 | 65 | suspend fun BiliClient.dynamicShortLink(did: String): String? = 66 | toShortLink(did, "dt.dt-detail.0.0.pv", "dynamic") 67 | 68 | suspend fun BiliClient.liveShortLink(rid: String): String? = 69 | toShortLink(rid, "live.live-room-detail.0.0.pv", "vertical-three-point-panel") 70 | 71 | suspend fun BiliClient.spaceShortLink(mid: String): String? = 72 | toShortLink(mid, "dt.space-dt.0.0.pv") 73 | 74 | suspend fun BiliClient.toShortLink(oid: String, shareId: String, shareOrigin: String? = null): String? { 75 | return try { 76 | useHttpClient { 77 | it.post(SHORT_LINK) { 78 | bodyParameter("build", "6880300") 79 | bodyParameter("buvid", "abcdefg") 80 | bodyParameter("platform", "android") 81 | bodyParameter("oid", oid) 82 | bodyParameter("share_channel", "QQ") 83 | bodyParameter("share_id", shareId) 84 | bodyParameter("share_mode", "3") 85 | if (shareOrigin != null) bodyParameter("share_origin", shareOrigin) 86 | }.body().decode().data?.decode()?.link 87 | } 88 | }catch (e: Exception) { null } 89 | } 90 | 91 | var lastWbiTime: LocalDate = LocalDate.now() 92 | var wbiImg: WbiImg? = null 93 | suspend fun getWbiImg(): WbiImg { 94 | val now = LocalDate.now() 95 | if (now.isAfter(lastWbiTime) || wbiImg == null) { 96 | lastWbiTime = now 97 | wbiImg = biliClient.userInfo()?.wbiImg 98 | } 99 | return wbiImg!! 100 | } 101 | 102 | suspend fun getVerifyString(): String { 103 | val wi = getWbiImg() 104 | val r = splitUrl(wi.imgUrl) + splitUrl(wi.subUrl) 105 | val array = intArrayOf(46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52) 106 | return buildString { 107 | array.forEach { t -> 108 | if (t < r.length) { 109 | append(r[t]) 110 | } 111 | } 112 | }.slice(IntRange(0, 31)) 113 | } 114 | 115 | fun splitUrl(url: String): String { 116 | return url.removeSuffix("/").split("/").last().split(".").first() 117 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/api/Live.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.api 2 | 3 | import io.ktor.client.request.* 4 | import top.colter.mirai.plugin.bilibili.client.BiliClient 5 | import top.colter.mirai.plugin.bilibili.data.LiveInfo 6 | import top.colter.mirai.plugin.bilibili.data.LiveList 7 | import top.colter.mirai.plugin.bilibili.data.LiveRoomDetail 8 | 9 | suspend fun BiliClient.getLive(page: Int = 1, pageSize: Int = 20): LiveList? { 10 | return getData(LIVE_LIST) { 11 | parameter("page", page) 12 | parameter("page_size", pageSize) 13 | } 14 | } 15 | 16 | suspend fun BiliClient.getLiveStatus(uids: List): Map? { 17 | return getData(LIVE_STATUS_BATCH) { 18 | for (uid in uids) { 19 | parameter("uids[]", uid) 20 | } 21 | } 22 | } 23 | 24 | suspend fun BiliClient.getLiveDetail(rid: String): LiveRoomDetail? { 25 | return getData(LIVE_DETAIL) { 26 | parameter("room_id", rid) 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/api/Pgc.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.api 2 | 3 | import io.ktor.client.request.* 4 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 5 | import top.colter.mirai.plugin.bilibili.client.BiliClient 6 | import top.colter.mirai.plugin.bilibili.data.* 7 | import top.colter.mirai.plugin.bilibili.service.pgcRegex 8 | import top.colter.mirai.plugin.bilibili.utils.bodyParameter 9 | import top.colter.mirai.plugin.bilibili.utils.decode 10 | 11 | 12 | internal suspend inline fun BiliClient.pgcGet( 13 | url: String, 14 | crossinline block: HttpRequestBuilder.() -> Unit = {} 15 | ): T? = get(url, block).result?.decode() 16 | 17 | 18 | suspend fun BiliClient.followPgc(ssid: Long): PgcFollow? { 19 | return post(FOLLOW_PGC) { 20 | bodyParameter("season_id", ssid) 21 | bodyParameter("csrf", BiliBiliDynamic.cookie.biliJct) 22 | }.result?.decode() 23 | } 24 | 25 | suspend fun BiliClient.unFollowPgc(ssid: Long): PgcFollow? { 26 | return post(UNFOLLOW_PGC) { 27 | bodyParameter("season_id", ssid) 28 | bodyParameter("csrf", BiliBiliDynamic.cookie.biliJct) 29 | }.result?.decode() 30 | } 31 | 32 | suspend fun BiliClient.getPcgInfo(id: String): BiliDetail? { 33 | val regex = pgcRegex.find(id) ?: return null 34 | 35 | val type = regex.destructured.component1() 36 | val id = regex.destructured.component2().toLong() 37 | 38 | return when (type) { 39 | "ss" -> getSeasonInfo(id) 40 | "md" -> getMediaInfo(id) 41 | "ep" -> getEpisodeInfo(id) 42 | else -> null 43 | } 44 | } 45 | 46 | suspend fun BiliClient.getMediaInfo(mdid: Long): PgcMedia? { 47 | return pgcGet(PGC_MEDIA_INFO) { 48 | parameter("media_id", mdid) 49 | } 50 | } 51 | 52 | suspend fun BiliClient.getEpisodeInfo(epid: Long): PgcSeason? { 53 | return pgcGet(PGC_INFO) { 54 | parameter("ep_id", epid) 55 | } 56 | } 57 | 58 | suspend fun BiliClient.getSeasonInfo(ssid: Long): PgcSeason? { 59 | return pgcGet(PGC_INFO) { 60 | parameter("season_id", ssid) 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/api/User.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.api 2 | 3 | import io.ktor.client.request.* 4 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 5 | import top.colter.mirai.plugin.bilibili.client.BiliClient 6 | import top.colter.mirai.plugin.bilibili.data.* 7 | import top.colter.mirai.plugin.bilibili.utils.bodyParameter 8 | import top.colter.mirai.plugin.bilibili.utils.decode 9 | 10 | suspend fun BiliClient.getLoginQrcode(): LoginQrcode? = getData(LOGIN_QRCODE) 11 | suspend fun BiliClient.loginInfo(qrcodeKey: String): LoginData? { 12 | return getData(LOGIN_INFO) { 13 | parameter("qrcode_key", qrcodeKey) 14 | } 15 | } 16 | 17 | suspend fun BiliClient.userInfo(uid: Long): BiliUser? { 18 | return getDataWithWbi(USER_INFO_WBI) { 19 | parameter("mid", uid) 20 | } 21 | } 22 | 23 | suspend fun BiliClient.userInfo(): BiliUser? { 24 | return getData(USER_ID) 25 | } 26 | 27 | suspend fun BiliClient.isFollow(uid: Long): IsFollow? { 28 | return getData(IS_FOLLOW) { 29 | parameter("fid", uid) 30 | } 31 | } 32 | 33 | suspend fun BiliClient.followGroup(): List? { 34 | return getData(GROUP_LIST) 35 | } 36 | 37 | suspend fun BiliClient.createGroup(tagName: String): FollowGroup? { 38 | return post(CREATE_GROUP) { 39 | bodyParameter("tag", tagName) 40 | bodyParameter("csrf", BiliBiliDynamic.cookie.biliJct) 41 | }.data?.decode() 42 | } 43 | 44 | suspend fun BiliClient.follow(uid: Long): BiliResult { 45 | return post(FOLLOW) { 46 | bodyParameter("fid", uid) 47 | bodyParameter("act", 1) 48 | bodyParameter("re_src", 11) 49 | bodyParameter("csrf", BiliBiliDynamic.cookie.biliJct) 50 | } 51 | } 52 | 53 | suspend fun BiliClient.groupAddUser(uid: Long, tagid: Int): BiliResult { 54 | return post(ADD_USER) { 55 | bodyParameter("fids", uid) 56 | bodyParameter("tagids", tagid) 57 | bodyParameter("csrf", BiliBiliDynamic.cookie.biliJct) 58 | } 59 | } 60 | 61 | suspend fun BiliClient.searchUser( 62 | keyword: String, 63 | order: String = "", 64 | orderSort: Int = 0, 65 | userType: Int = 0, 66 | page: Int = 1, 67 | pageSize: Int = 20 68 | ): BiliSearch? { 69 | return getData(SEARCH) { 70 | parameter("page", page) 71 | parameter("page_size", pageSize) 72 | parameter("search_type", "bili_user") // bili_user video media_bangumi media_ft live article topic 73 | parameter("keyword", keyword) 74 | parameter("order", order) // 空 fans level 75 | parameter("order_sort", orderSort) // 0: 由高到低 1: 由低到高 76 | parameter("user_type", userType) // 0: 全部用户 1: UP主用户 2: 普通用户 3: 认证用户 77 | } 78 | } 79 | 80 | suspend fun BiliClient.searchUserVideo( 81 | uid: Long, 82 | count: Int = 1, 83 | order: String = "pubdate", 84 | ): VideoList? { 85 | return getData(SPACE_SEARCH) { 86 | parameter("mid", uid) 87 | parameter("ps", count) 88 | parameter("order", order) 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/client/BiliClient.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.client 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.call.* 5 | import io.ktor.client.engine.* 6 | import io.ktor.client.engine.okhttp.* 7 | import io.ktor.client.plugins.* 8 | import io.ktor.client.request.* 9 | import io.ktor.http.* 10 | import io.ktor.utils.io.core.* 11 | import kotlinx.coroutines.CancellationException 12 | import kotlinx.coroutines.isActive 13 | import kotlinx.coroutines.supervisorScope 14 | import kotlinx.serialization.json.Json 15 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 16 | import top.colter.mirai.plugin.bilibili.BiliConfig.checkConfig 17 | import top.colter.mirai.plugin.bilibili.BiliConfig.enableConfig 18 | import top.colter.mirai.plugin.bilibili.BiliConfig.proxyConfig 19 | import top.colter.mirai.plugin.bilibili.utils.decode 20 | import top.colter.mirai.plugin.bilibili.utils.isNotBlank 21 | import top.colter.mirai.plugin.bilibili.utils.json 22 | import java.io.IOException 23 | 24 | open class BiliClient : Closeable { 25 | override fun close() = clients.forEach { it.close() } 26 | 27 | private val proxys = if (proxyConfig.proxy.isNotBlank()) { 28 | mutableListOf().apply { 29 | proxyConfig.proxy.forEach { 30 | if (it != "") { 31 | add(ProxyBuilder.http(it)) 32 | } 33 | } 34 | } 35 | } else { 36 | null 37 | } 38 | 39 | val clients = MutableList(3) { client() } 40 | 41 | protected fun client() = HttpClient(OkHttp) { 42 | defaultRequest { 43 | header(HttpHeaders.Origin, "https://t.bilibili.com") 44 | header(HttpHeaders.Referrer, "https://t.bilibili.com") 45 | } 46 | install(HttpTimeout) { 47 | socketTimeoutMillis = checkConfig.timeout * 1000L 48 | connectTimeoutMillis = checkConfig.timeout * 1000L 49 | requestTimeoutMillis = checkConfig.timeout * 1000L 50 | } 51 | expectSuccess = true 52 | Json { 53 | json 54 | } 55 | //BrowserUserAgent() 56 | install(UserAgent) { 57 | agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" 58 | } 59 | } 60 | 61 | suspend inline fun get(url: String, crossinline block: HttpRequestBuilder.() -> Unit = {}): T = 62 | useHttpClient { 63 | it.get(url) { 64 | header(HttpHeaders.Cookie, BiliBiliDynamic.cookie.toString() + "DedeUserID=" + BiliBiliDynamic.uid) 65 | block() 66 | }.body() 67 | }.decode() 68 | 69 | suspend inline fun post(url: String, crossinline block: HttpRequestBuilder.() -> Unit = {}): T = 70 | useHttpClient { 71 | it.post(url) { 72 | header(HttpHeaders.Cookie, BiliBiliDynamic.cookie.toString() + "DedeUserID=" + BiliBiliDynamic.uid) 73 | block() 74 | }.body() 75 | }.decode() 76 | 77 | private var clientIndex = 0 78 | private var proxyIndex = 0 79 | 80 | suspend fun useHttpClient(block: suspend (HttpClient) -> T): T = supervisorScope { 81 | while (isActive) { 82 | try { 83 | val client = clients[clientIndex] 84 | if (proxys != null && enableConfig.proxyEnable) { 85 | client.engineConfig.proxy = proxys[proxyIndex] 86 | proxyIndex = (proxyIndex + 1) % proxys.size 87 | } 88 | return@supervisorScope block(client) 89 | } catch (throwable: Throwable) { 90 | if (isActive && (throwable is IOException || throwable is HttpRequestTimeoutException)) { 91 | clientIndex = (clientIndex + 1) % clients.size 92 | } else { 93 | throw throwable 94 | } 95 | } 96 | } 97 | throw CancellationException() 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/command/GroupOrContact.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.command 2 | 3 | import net.mamoe.mirai.contact.Contact 4 | import top.colter.mirai.plugin.bilibili.Group 5 | import top.colter.mirai.plugin.bilibili.utils.delegate 6 | import top.colter.mirai.plugin.bilibili.utils.name 7 | 8 | data class GroupOrContact( 9 | val contact: Contact? = null, 10 | val group: Group? = null, 11 | ) 12 | 13 | val GroupOrContact.isGroup: Boolean 14 | get() = group != null 15 | 16 | val GroupOrContact.subject: String 17 | get() = group?.name ?: contact!!.delegate 18 | 19 | val GroupOrContact.name: String 20 | get() = group?.name ?: contact!!.name 21 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/command/GroupOrContactParser.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.command 2 | 3 | import net.mamoe.mirai.console.command.CommandSender 4 | import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser 5 | import net.mamoe.mirai.console.command.descriptor.ExistingContactValueArgumentParser 6 | import top.colter.mirai.plugin.bilibili.BiliData 7 | 8 | object GroupOrContactParser: CommandValueArgumentParser { 9 | 10 | override fun parse(raw: String, sender: CommandSender): GroupOrContact { 11 | val group = BiliData.group[raw] 12 | return GroupOrContact( 13 | if (group == null) ExistingContactValueArgumentParser.parse(raw, sender) else null, 14 | group 15 | ) 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Article.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | data class ArticleDetail( 9 | @SerialName("id") 10 | val aid: Long, 11 | @SerialName("title") 12 | val title: String, 13 | @SerialName("summary") 14 | val summary: String, 15 | @SerialName("author") 16 | val author: ModuleAuthor, 17 | @SerialName("image_urls") 18 | val covers: List, 19 | @SerialName("publish_time") 20 | val time: Long, 21 | @SerialName("stats") 22 | val stats: Stats, 23 | @SerialName("words") 24 | val words: Int, 25 | ): BiliDetail 26 | 27 | @Serializable 28 | data class ArticleInfo( 29 | @SerialName("like") 30 | val like: Int? = null, 31 | @SerialName("attention") 32 | val attention: Boolean? = null, 33 | @SerialName("favorite") 34 | val favorite: Boolean? = null, 35 | @SerialName("coin") 36 | val coin: Int? = null, 37 | @SerialName("stats") 38 | val stats: Stats? = null, 39 | @SerialName("title") 40 | val title: String, 41 | @SerialName("banner_url") 42 | val bannerUrl: String? = null, 43 | @SerialName("mid") 44 | val mid: Long, 45 | @SerialName("author_name") 46 | val authorName: String, 47 | @SerialName("is_author") 48 | val isAuthor: Boolean? = null, 49 | @SerialName("image_urls") 50 | val imageUrls: List? = null, 51 | @SerialName("origin_image_urls") 52 | val originImageUrls: List? = null, 53 | @SerialName("shareable") 54 | val shareable: Boolean? = null, 55 | @SerialName("show_later_watch") 56 | val showLaterWatch: Boolean? = null, 57 | @SerialName("show_small_window") 58 | val showSmallWindow: Boolean? = null, 59 | @SerialName("in_list") 60 | val inList: Boolean? = null, 61 | @SerialName("pre") 62 | val pre: Int? = null, 63 | @SerialName("next") 64 | val next: Int? = null, 65 | @SerialName("type") 66 | val type: Int? = null, 67 | @SerialName("video_url") 68 | val videoUrl: String? = null, 69 | @SerialName("location") 70 | val location: String? = null, 71 | @SerialName("disable_share") 72 | val disableShare: Boolean? = null, 73 | ): BiliDetail 74 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/BascLink.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import top.colter.mirai.plugin.bilibili.BiliConfig 4 | import top.colter.mirai.plugin.bilibili.api.articleShortLink 5 | import top.colter.mirai.plugin.bilibili.api.dynamicShortLink 6 | import top.colter.mirai.plugin.bilibili.api.liveShortLink 7 | import top.colter.mirai.plugin.bilibili.api.spaceShortLink 8 | import top.colter.mirai.plugin.bilibili.utils.biliClient 9 | 10 | const val BASE_LINK = "https://www.bilibili.com" 11 | const val BASE_DYNAMIC = "https://t.bilibili.com" 12 | const val BASE_ARTICLE = "https://www.bilibili.com/read" 13 | const val BASE_VIDEO = "https://www.bilibili.com/video" 14 | const val BASE_MUSIC = "https://www.bilibili.com/audio" 15 | const val BASE_PGC = "https://www.bilibili.com/bangumi/play" 16 | const val BASE_PGC_MEDIA = "https://www.bilibili.com/bangumi/media" 17 | const val BASE_LIVE = "https://live.bilibili.com" 18 | const val BASE_SPACE = "https://space.bilibili.com" 19 | const val BASE_SHORT = "b23.tv" 20 | 21 | val toShortLink: Boolean by lazy { BiliConfig.pushConfig.toShortLink } 22 | 23 | suspend fun DYNAMIC_LINK(id: String) = 24 | if (toShortLink) biliClient.dynamicShortLink(id).run { 25 | this?.removePrefix("https://") ?: "$BASE_DYNAMIC/$id" 26 | } else "$BASE_DYNAMIC/$id" 27 | 28 | suspend fun OPUS_LINK(id: String) = 29 | if (toShortLink) biliClient.dynamicShortLink(id).run { 30 | this?.removePrefix("https://") ?: "$BASE_LINK/opus/$id" 31 | } else "$BASE_LINK/opus/$id" 32 | 33 | suspend fun ARTICLE_LINK(id: String) = 34 | if (toShortLink) { 35 | biliClient.articleShortLink(id).run { 36 | this?.removePrefix("https://") ?: "$BASE_ARTICLE/cv$id" 37 | } 38 | }else "$BASE_ARTICLE/cv$id" 39 | 40 | fun VIDEO_LINK(id: String): String { 41 | val tid = if (id.contains("BV") || id.contains("av")) id else "av$id" 42 | return if (toShortLink) "$BASE_SHORT/$tid" else "$BASE_VIDEO/$tid" 43 | } 44 | 45 | suspend fun SPACE_LINK(id: String): String = if (toShortLink) { 46 | biliClient.spaceShortLink(id).run { 47 | this?.removePrefix("https://") ?: "$BASE_SPACE/$id" 48 | } 49 | } else "$BASE_SPACE/$id" 50 | 51 | fun MUSIC_LINK(id: String) = "$BASE_MUSIC/au$id" 52 | fun MEDIA_LINK(id: String) = "$BASE_PGC_MEDIA/md$id" 53 | fun SEASON_LINK(id: String) = if (toShortLink) "$BASE_SHORT/ss$id" else "$BASE_PGC/ss$id" 54 | fun EPISODE_LINK(id: String) = if (toShortLink) "$BASE_SHORT/ep$id" else "$BASE_PGC/ep$id" 55 | fun PGC_LINK(id: String) = if (toShortLink) "$BASE_SHORT/$id" else if (id.startsWith("md")) "$BASE_PGC_MEDIA/$id" else "$BASE_PGC/$id" 56 | suspend fun LIVE_LINK(id: String) = if (toShortLink) { 57 | biliClient.liveShortLink(id).run { 58 | this?.removePrefix("https://") ?: "$BASE_LIVE/$id" 59 | } 60 | }else "$BASE_LIVE/$id" 61 | 62 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/BiliCookie.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class BiliCookie( 8 | @SerialName("SESSDATA") 9 | var sessData: String = "", 10 | @SerialName("bili_jct") 11 | var biliJct: String = "" 12 | ) { 13 | fun parse(cookie: String): BiliCookie { 14 | cookie.split("; ", ";").forEach { 15 | val cookieKV = it.split("=") 16 | if (cookieKV[0] == "SESSDATA") sessData = cookieKV[1].replace(",", "%2C").replace("*", "%2A") 17 | if (cookieKV[0] == "bili_jct") biliJct = cookieKV[1] 18 | } 19 | return this 20 | } 21 | 22 | fun isEmpty(): Boolean = sessData == "" && biliJct == "" 23 | 24 | override fun toString(): String { 25 | return "SESSDATA=$sessData; bili_jct=$biliJct; " 26 | } 27 | } 28 | 29 | @Serializable 30 | data class EditThisCookie( 31 | @SerialName("domain") 32 | val domain: String, 33 | @SerialName("expirationDate") 34 | val expirationDate: Double? = null, 35 | @SerialName("hostOnly") 36 | val hostOnly: Boolean = false, 37 | @SerialName("httpOnly") 38 | val httpOnly: Boolean, 39 | @SerialName("id") 40 | val id: Int, 41 | @SerialName("name") 42 | val name: String, 43 | @SerialName("path") 44 | val path: String, 45 | @SerialName("sameSite") 46 | val sameSite: String = "unspecified", 47 | @SerialName("secure") 48 | val secure: Boolean, 49 | @SerialName("session") 50 | val session: Boolean = false, 51 | @SerialName("storeId") 52 | val storeId: String = "0", 53 | @SerialName("value") 54 | val value: String 55 | ) 56 | 57 | fun List.toCookie(): BiliCookie { 58 | val bc = BiliCookie() 59 | for (cookie in this) { 60 | if (cookie.name == "SESSDATA") bc.sessData = cookie.value 61 | if (cookie.name == "bili_jct") bc.biliJct = cookie.value 62 | } 63 | return bc 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/BiliDetail.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | sealed interface BiliDetail { 7 | //fun drawGeneral() {} 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/BiliMessage.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | sealed interface BiliMessage { 8 | val mid: Long 9 | val name: String 10 | val time: String 11 | val timestamp: Int 12 | val drawPath: String? 13 | val contact: String? 14 | } 15 | 16 | @Serializable 17 | data class DynamicMessage( 18 | val did: String, 19 | override val mid: Long, 20 | override val name: String, 21 | val type: DynamicType, 22 | override val time: String, 23 | override val timestamp: Int, 24 | val content: String, 25 | val images: List?, 26 | val links: List?, 27 | override val drawPath: String? = null, 28 | override val contact: String? = null 29 | ) : BiliMessage { 30 | @Serializable 31 | data class Link( 32 | val tag: String, 33 | val value: String, 34 | ) 35 | } 36 | 37 | @Serializable 38 | data class LiveMessage( 39 | val rid: Long, 40 | override val mid: Long, 41 | override val name: String, 42 | override val time: String, 43 | override val timestamp: Int, 44 | val title: String, 45 | val cover: String, 46 | val area: String, 47 | val link: String, 48 | override val drawPath: String? = null, 49 | override val contact: String? = null 50 | ) : BiliMessage 51 | 52 | @Serializable 53 | data class LiveCloseMessage( 54 | val rid: Long, 55 | override val mid: Long, 56 | override val name: String, 57 | override val time: String, 58 | override val timestamp: Int, 59 | val endTime: String, 60 | val duration: String, 61 | val title: String, 62 | val area: String, 63 | val link: String, 64 | override val drawPath: String? = null, 65 | override val contact: String? = null 66 | ) : BiliMessage -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/BiliSearch.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class BiliSearch( 8 | @SerialName("seid") 9 | val seid: Float? = null, 10 | @SerialName("page") 11 | val page: Int? = null, 12 | @SerialName("pagesize") 13 | val pagesize: Int? = null, 14 | @SerialName("numResults") 15 | val numResults: Int? = null, 16 | @SerialName("numPages") 17 | val numPages: Int? = null, 18 | @SerialName("suggest_keyword") 19 | val suggestKeyword: String? = null, 20 | @SerialName("rqt_type") 21 | val rqtType: String? = null, 22 | @SerialName("cost_time") 23 | val costTime: CostTime? = null, 24 | @SerialName("exp_list") 25 | val expList: String? = null, 26 | @SerialName("egg_hit") 27 | val eggHit: Int? = null, 28 | @SerialName("result") 29 | val result: List? = null, 30 | @SerialName("show_column") 31 | val showColumn: Int? = null, 32 | @SerialName("in_black_key") 33 | val inBlackKey: Int? = null, 34 | @SerialName("in_white_key") 35 | val inWhiteKey: Int? = null, 36 | ) { 37 | @Serializable 38 | data class CostTime( 39 | @SerialName("params_check") 40 | val paramsCheck: Float? = null, 41 | @SerialName("get_upuser_live_status") 42 | val getUpuserLiveStatus: Float? = null, 43 | @SerialName("illegal_handler") 44 | val illegalHandler: Float? = null, 45 | @SerialName("as_response_format") 46 | val asResponseFormat: Float? = null, 47 | @SerialName("as_request") 48 | val asRequest: Float? = null, 49 | @SerialName("save_cache") 50 | val saveCache: Float? = null, 51 | @SerialName("deserialize_response") 52 | val deserializeResponse: Float? = null, 53 | @SerialName("as_request_format") 54 | val asRequestFormat: Float? = null, 55 | @SerialName("total") 56 | val total: Float? = null, 57 | @SerialName("main_handler") 58 | val mainHandler: Float? = null, 59 | ) 60 | 61 | @Serializable 62 | data class SearchResult( 63 | @SerialName("type") 64 | val type: String? = null, 65 | @SerialName("mid") 66 | val mid: Long? = null, 67 | @SerialName("uname") 68 | val uname: String? = null, 69 | @SerialName("usign") 70 | val usign: String? = null, 71 | @SerialName("fans") 72 | val fans: Int? = null, 73 | @SerialName("videos") 74 | val videos: Int? = null, 75 | @SerialName("upic") 76 | val upic: String? = null, 77 | @SerialName("face_nft") 78 | val faceNft: Int? = null, 79 | @SerialName("face_nft_type") 80 | val faceNftType: Int? = null, 81 | @SerialName("verify_info") 82 | val verifyInfo: String? = null, 83 | @SerialName("level") 84 | val level: Int? = null, 85 | @SerialName("gender") 86 | val gender: Int? = null, 87 | @SerialName("is_upuser") 88 | val isUpuser: Int? = null, 89 | @SerialName("is_live") 90 | val isLive: Int? = null, 91 | @SerialName("room_id") 92 | val roomId: Int? = null, 93 | @SerialName("res") 94 | val res: List? = null, 95 | @SerialName("official_verify") 96 | val officialVerify: OfficialVerify? = null, 97 | @SerialName("hit_columns") 98 | val hitColumns: List? = null, 99 | ) { 100 | @Serializable 101 | data class Res( 102 | @SerialName("aid") 103 | val aid: Int? = null, 104 | @SerialName("bvid") 105 | val bvid: String? = null, 106 | @SerialName("title") 107 | val title: String? = null, 108 | @SerialName("pubdate") 109 | val pubdate: Int? = null, 110 | @SerialName("arcurl") 111 | val arcurl: String? = null, 112 | @SerialName("pic") 113 | val pic: String? = null, 114 | @SerialName("play") 115 | val play: Int? = null, 116 | @SerialName("dm") 117 | val dm: Int? = null, 118 | @SerialName("coin") 119 | val coin: Int? = null, 120 | @SerialName("fav") 121 | val fav: Int? = null, 122 | @SerialName("desc") 123 | val desc: String? = null, 124 | @SerialName("duration") 125 | val duration: String? = null, 126 | @SerialName("is_pay") 127 | val isPay: Int? = null, 128 | @SerialName("is_union_video") 129 | val isUnionVideo: Int? = null, 130 | ) 131 | 132 | @Serializable 133 | data class OfficialVerify( 134 | @SerialName("type") 135 | val type: Int? = null, 136 | @SerialName("desc") 137 | val desc: String? = null, 138 | ) 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/BiliUser.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class BiliUser( 8 | @SerialName("mid") 9 | val mid: Long, 10 | @SerialName("name") 11 | val name: String? = "", 12 | @SerialName("face") 13 | val face: String? = "", 14 | @SerialName("official") 15 | val official: ModuleAuthor.OfficialVerify? = null, 16 | @SerialName("vip") 17 | val vip: ModuleAuthor.Vip? = null, 18 | @SerialName("pendant") 19 | val pendant: ModuleAuthor.Pendant? = null, 20 | @SerialName("wbi_img") 21 | val wbiImg: WbiImg? = null, 22 | 23 | //@SerialName("sex") 24 | //val sex: String, 25 | //@SerialName("face_nft") 26 | //val faceNft: Int, 27 | //@SerialName("face_nft_type") 28 | //val faceNftType: Int, 29 | //@SerialName("rank") 30 | //val rank: Int, 31 | //@SerialName("level") 32 | //val level: Int, 33 | //@SerialName("moral") 34 | //val moral: Int, 35 | //@SerialName("silence") 36 | //val silence: Int, 37 | //@SerialName("coins") 38 | //val coins: Int, 39 | //@SerialName("is_followed") 40 | //val isFollowed: Boolean, 41 | //@SerialName("top_photo") 42 | //val top_photo: String, 43 | //@SerialName("birthday") 44 | //val birthday: String, 45 | //@SerialName("is_senior_member") 46 | //val isSeniorMember: Int, 47 | //@SerialName("fans_badge") 48 | //val fansBadge: Boolean, 49 | //@SerialName("fans_medal") 50 | //val fansMedal: FansMedal, 51 | //@SerialName("nameplate") 52 | //val nameplate: Nameplate, 53 | //@SerialName("user_honour_info") 54 | //val userHonourInfo: UserHonourInfo, 55 | //@SerialName("live_room") 56 | //val liveRoom: LiveRoom, 57 | //@SerialName("school") 58 | //val school: School, 59 | //@SerialName("profession") 60 | //val profession: Profession, 61 | //@SerialName("tags") 62 | //val tags: List, 63 | //@SerialName("series") 64 | //val series: Series, 65 | 66 | //@SerialName("theme") 67 | //val theme: ?, 68 | //@SerialName("sys_notice") 69 | //val sysNotice: ?, 70 | 71 | ): BiliDetail { 72 | @Serializable 73 | data class Nameplate( 74 | @SerialName("nid") 75 | val nid: Int, 76 | @SerialName("name") 77 | val name: String, 78 | @SerialName("image") 79 | val image: String, 80 | @SerialName("image_small") 81 | val imageSmall: String, 82 | @SerialName("level") 83 | val level: String, 84 | @SerialName("condition") 85 | val condition: String 86 | ) 87 | 88 | @Serializable 89 | data class UserHonourInfo( 90 | @SerialName("mid") 91 | val mid: Long, 92 | @SerialName("colour") 93 | val colour: String?, 94 | @SerialName("tags") 95 | val tags: List, 96 | ) 97 | 98 | @Serializable 99 | data class School( 100 | @SerialName("name") 101 | val name: String, 102 | ) 103 | 104 | @Serializable 105 | data class Profession( 106 | @SerialName("name") 107 | val name: String, 108 | @SerialName("department") 109 | val department: String, 110 | @SerialName("title") 111 | val title: String, 112 | @SerialName("is_show") 113 | val isShow: Int, 114 | ) 115 | 116 | @Serializable 117 | data class Series( 118 | @SerialName("user_upgrade_status") 119 | val userUpgradeStatus: Int, 120 | @SerialName("show_upgrade_window") 121 | val showUpgradeWindow: Boolean, 122 | ) 123 | 124 | } 125 | 126 | @Serializable 127 | data class WbiImg( 128 | @SerialName("img_url") 129 | val imgUrl: String, 130 | @SerialName("sub_url") 131 | val subUrl: String, 132 | ) 133 | 134 | @Serializable 135 | data class FansMedal( 136 | @SerialName("show") 137 | val show: Boolean, 138 | @SerialName("wear") 139 | val wear: Boolean, 140 | @SerialName("medal") 141 | val medal: Medal, 142 | ) { 143 | @Serializable 144 | data class Medal( 145 | @SerialName("uid") 146 | val uid: Long, 147 | @SerialName("target_id") 148 | val targetId: Long, 149 | @SerialName("medal_id") 150 | val medalId: Long, 151 | @SerialName("level") 152 | val level: Int, 153 | @SerialName("medal_name") 154 | val medalName: String, 155 | @SerialName("intimacy") 156 | val intimacy: Int, 157 | @SerialName("next_intimacy") 158 | val nextIntimacy: Int, 159 | @SerialName("day_limit") 160 | val dayLimit: Int, 161 | @SerialName("medal_color") 162 | val medalColor: Int, 163 | @SerialName("medal_color_start") 164 | val medalColorStart: Int, 165 | @SerialName("medal_color_end") 166 | val medalColorEnd: Int, 167 | @SerialName("medal_color_border") 168 | val medalColorBorder: Int, 169 | @SerialName("is_lighted") 170 | val isLighted: Int, 171 | @SerialName("light_status") 172 | val lightStatus: Int, 173 | @SerialName("wearing_status") 174 | val wearingStatus: Int, 175 | @SerialName("score") 176 | val score: Int, 177 | ) 178 | } 179 | 180 | @Serializable 181 | data class Official( 182 | @SerialName("role") 183 | val role: Int, 184 | @SerialName("title") 185 | val title: String, 186 | @SerialName("desc") 187 | val desc: String, 188 | @SerialName("type") 189 | val type: Int, 190 | ) 191 | 192 | @Serializable 193 | data class LiveRoom( 194 | @SerialName("roomStatus") 195 | val roomStatus: Int, 196 | @SerialName("liveStatus") 197 | val liveStatus: Int, 198 | @SerialName("url") 199 | val url: String, 200 | @SerialName("title") 201 | val title: String, 202 | @SerialName("cover") 203 | val cover: String, 204 | @SerialName("roomid") 205 | val roomid: Long, 206 | @SerialName("roundStatus") 207 | val roundStatus: Int, 208 | @SerialName("broadcast_type") 209 | val broadcastType: Int, 210 | @SerialName("watched_show") 211 | val watchedShow: WatchedShow, 212 | ) { 213 | @Serializable 214 | data class WatchedShow( 215 | @SerialName("switch") 216 | val switch: Boolean, 217 | @SerialName("num") 218 | val num: Int, 219 | @SerialName("text_small") 220 | val textSmall: Int, 221 | @SerialName("text_large") 222 | val textLarge: String, 223 | @SerialName("icon") 224 | val icon: String, 225 | @SerialName("icon_location") 226 | val iconLocation: String, 227 | @SerialName("icon_web") 228 | val iconWeb: String, 229 | ) 230 | } 231 | 232 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/DynamicImageQuality.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.Serializable 4 | import net.mamoe.mirai.console.data.ReadOnlyPluginConfig 5 | import net.mamoe.mirai.console.data.ValueDescription 6 | import net.mamoe.mirai.console.data.value 7 | 8 | object BiliImageQuality : ReadOnlyPluginConfig("ImageQuality") { 9 | 10 | @ValueDescription("具体的配置文件描述请前往下方链接查看") 11 | val help: String by value("https://github.com/Colter23/bilibili-dynamic-mirai-plugin#ImageQuality.yml") 12 | 13 | @ValueDescription("是否启用自定义数据\n启用后配置文件中的分辨率配置将失效") 14 | val customOverload: Boolean by value(false) 15 | 16 | @ValueDescription("自定义图片分辨率\n默认数据为1000px宽度下的数据") 17 | val customQuality: Quality by value(quality["1000w"]!!) 18 | 19 | val quality: Map 20 | get() = mapOf( 21 | "800w" to Quality( 22 | imageWidth = 800, 23 | cardMargin = 20, 24 | cardPadding = 20, 25 | cardArc = 10f, 26 | 27 | nameFontSize = 30f, 28 | titleFontSize = 26f, 29 | subTitleFontSize = 22f, 30 | descFontSize = 20f, 31 | contentFontSize = 26f, 32 | footerFontSize = 22f, 33 | 34 | cardOutlineWidth = 2f, 35 | drawOutlineWidth = 2f, 36 | 37 | faceSize = 64f, 38 | noPendantFaceInflate = 5f, 39 | pendantSize = 112f, 40 | verifyIconSize = 20f, 41 | ornamentHeight = 90f, 42 | 43 | badgeHeight = 36, 44 | badgePadding = 5, 45 | badgeArc = 5f, 46 | 47 | lineSpace = 8, 48 | drawSpace = 10, 49 | contentSpace = 10, 50 | 51 | smallCardHeight = 160, 52 | additionalCardHeight = 90 53 | ), 54 | "1000w" to Quality( 55 | imageWidth = 1000, 56 | cardMargin = 30, 57 | cardPadding = 30, 58 | cardArc = 15f, 59 | 60 | nameFontSize = 36f, 61 | titleFontSize = 32f, 62 | subTitleFontSize = 28f, 63 | descFontSize = 26f, 64 | contentFontSize = 32f, 65 | footerFontSize = 28f, 66 | 67 | cardOutlineWidth = 3f, 68 | drawOutlineWidth = 3f, 69 | 70 | faceSize = 80f, 71 | noPendantFaceInflate = 10f, 72 | pendantSize = 140f, 73 | verifyIconSize = 30f, 74 | ornamentHeight = 125f, 75 | 76 | badgeHeight = 45, 77 | badgePadding = 8, 78 | badgeArc = 8f, 79 | 80 | lineSpace = 11, 81 | drawSpace = 15, 82 | contentSpace = 12, 83 | 84 | smallCardHeight = 200, 85 | additionalCardHeight = 130 86 | ), 87 | "1200w" to Quality( 88 | imageWidth = 1200, 89 | cardMargin = 40, 90 | cardPadding = 40, 91 | cardArc = 20f, 92 | 93 | nameFontSize = 42f, 94 | titleFontSize = 38f, 95 | subTitleFontSize = 34f, 96 | descFontSize = 32f, 97 | contentFontSize = 38f, 98 | footerFontSize = 34f, 99 | 100 | cardOutlineWidth = 4f, 101 | drawOutlineWidth = 4f, 102 | 103 | faceSize = 95f, 104 | noPendantFaceInflate = 13f, 105 | pendantSize = 170f, 106 | verifyIconSize = 40f, 107 | ornamentHeight = 140f, 108 | 109 | badgeHeight = 55, 110 | badgePadding = 11, 111 | badgeArc = 11f, 112 | 113 | lineSpace = 14, 114 | drawSpace = 20, 115 | contentSpace = 17, 116 | 117 | smallCardHeight = 240, 118 | additionalCardHeight = 160 119 | ), 120 | "1500w" to Quality( 121 | imageWidth = 1500, 122 | cardMargin = 50, 123 | cardPadding = 50, 124 | cardArc = 30f, 125 | 126 | nameFontSize = 51f, 127 | titleFontSize = 46f, 128 | subTitleFontSize = 43f, 129 | descFontSize = 40f, 130 | contentFontSize = 47f, 131 | footerFontSize = 43f, 132 | 133 | cardOutlineWidth = 6f, 134 | drawOutlineWidth = 6f, 135 | 136 | faceSize = 100f, 137 | noPendantFaceInflate = 18f, 138 | pendantSize = 190f, 139 | verifyIconSize = 50f, 140 | ornamentHeight = 150f, 141 | 142 | badgeHeight = 72, 143 | badgePadding = 15, 144 | badgeArc = 16f, 145 | 146 | lineSpace = 20, 147 | drawSpace = 25, 148 | contentSpace = 20, 149 | 150 | smallCardHeight = 300, 151 | additionalCardHeight = 205 152 | ) 153 | ) 154 | 155 | } 156 | 157 | @Serializable 158 | data class Quality( 159 | val imageWidth: Int, 160 | val cardMargin: Int, 161 | val cardPadding: Int, 162 | val cardArc: Float, 163 | 164 | val nameFontSize: Float, 165 | val titleFontSize: Float, 166 | val subTitleFontSize: Float, 167 | val descFontSize: Float, 168 | val contentFontSize: Float, 169 | val footerFontSize: Float, 170 | 171 | val cardOutlineWidth: Float, 172 | val drawOutlineWidth: Float, 173 | 174 | val faceSize: Float, 175 | val noPendantFaceInflate: Float, 176 | val pendantSize: Float, 177 | val verifyIconSize: Float, 178 | val ornamentHeight: Float, 179 | 180 | var badgeHeight: Int, 181 | val badgePadding: Int, 182 | val badgeArc: Float, 183 | 184 | val lineSpace: Int, 185 | val drawSpace: Int, 186 | val contentSpace: Int, 187 | 188 | val smallCardHeight: Int, 189 | val additionalCardHeight: Int 190 | ) -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/DynamicImageTheme.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.Serializable 4 | import net.mamoe.mirai.console.data.ReadOnlyPluginConfig 5 | import net.mamoe.mirai.console.data.ValueDescription 6 | import net.mamoe.mirai.console.data.value 7 | import org.jetbrains.skia.Color 8 | import top.colter.mirai.plugin.bilibili.draw.makeRGB 9 | 10 | 11 | object BiliImageTheme : ReadOnlyPluginConfig("ImageTheme") { 12 | 13 | @ValueDescription("具体的配置文件描述请前往下方链接查看") 14 | val help: String by value("https://github.com/Colter23/bilibili-dynamic-mirai-plugin#ImageTheme.yml") 15 | 16 | @ValueDescription("是否启用自定义数据\n启用后配置文件中的主题配置将失效") 17 | val customOverload: Boolean by value(false) 18 | 19 | @ValueDescription("自定义图片主题\n默认数据为v3主题数据") 20 | val customTheme: Theme by value(theme["v3"]!!) 21 | 22 | @ValueDescription("图片主题") 23 | val theme: Map 24 | get() = mapOf( 25 | "v3" to Theme( 26 | "#B4FFFFFF", 27 | "#FFFFFF", 28 | "#A0FFFFFF", 29 | "#FFFFFF", 30 | "#FB7299", 31 | "#313131", 32 | "#9C9C9C", 33 | "#666666", 34 | "#222222", 35 | "#178BCF", 36 | "#9C9C9C", 37 | Theme.Shadow("#46000000", 6f, 6f, 25f, 0f), 38 | Theme.Shadow("#1E000000", 5f, 5f, 15f, 0f), 39 | Theme.BadgeColor("#00CBFF", "#B4FFFFFF"), 40 | Theme.BadgeColor("#FFFFFF", "#48C7F0"), 41 | Theme.BadgeColor("#FFFFFF", "#FB7299"), 42 | Theme.BadgeColor("#FFFFFF", "#48C7F0"), 43 | ), 44 | "v3RainbowOutline" to Theme( 45 | "#B4FFFFFF", 46 | "#ff0000;#ff00ff;#0000ff;#00ffff;#00ff00;#ffff00;#ff0000", 47 | "#A0FFFFFF", 48 | "#FFFFFF", 49 | "#FB7299", 50 | "#313131", 51 | "#9C9C9C", 52 | "#666666", 53 | "#222222", 54 | "#178BCF", 55 | "#9C9C9C", 56 | Theme.Shadow("#46000000", 6f, 6f, 25f, 0f), 57 | Theme.Shadow("#1E000000", 5f, 5f, 15f, 0f), 58 | Theme.BadgeColor("#00CBFF", "#B4FFFFFF"), 59 | Theme.BadgeColor("#FFFFFF", "#48C7F0"), 60 | Theme.BadgeColor("#FFFFFF", "#FB7299"), 61 | Theme.BadgeColor("#FFFFFF", "#48C7F0"), 62 | ), 63 | "v2" to Theme( 64 | "#C8FFFFFF", 65 | "#FFFFFF", 66 | "#A0FFFFFF", 67 | "#FFFFFF", 68 | "#FB7299", 69 | "#313131", 70 | "#9C9C9C", 71 | "#666666", 72 | "#222222", 73 | "#178BCF", 74 | "#9C9C9C", 75 | Theme.Shadow("#00000000", 0f, 0f, 0f, 0f), 76 | Theme.Shadow("#00000000", 0f, 0f, 0f, 0f), 77 | Theme.BadgeColor("#00CBFF", "#C8FFFFFF"), 78 | Theme.BadgeColor("#FFFFFF", "#48C7F0"), 79 | Theme.BadgeColor("#FFFFFF", "#FB7299"), 80 | Theme.BadgeColor("#FFFFFF", "#48C7F0"), 81 | ) 82 | ) 83 | } 84 | 85 | @Serializable 86 | data class Theme( 87 | val cardBgColorHex: String, 88 | 89 | val cardOutlineColorHex: String, 90 | val faceOutlineColorHex: String, 91 | val drawOutlineColorHex: String, 92 | 93 | val nameColorHex: String, 94 | val titleColorHex: String, 95 | val subTitleColorHex: String, 96 | val descColorHex: String, 97 | val contentColorHex: String, 98 | val linkColorHex: String, 99 | val footerColorHex: String, 100 | 101 | val cardShadow: Shadow, 102 | val smallCardShadow: Shadow, 103 | 104 | val mainLeftBadge: BadgeColor, 105 | val mainRightBadge: BadgeColor, 106 | val subLeftBadge: BadgeColor, 107 | val subRightBadge: BadgeColor, 108 | 109 | ) { 110 | 111 | @Serializable 112 | data class Shadow( 113 | val shadowColorHex: String, 114 | val offsetX: Float, 115 | val offsetY: Float, 116 | val blur: Float, 117 | val spread: Float = 0f, 118 | ) { 119 | val shadowColor: Int get() = Color.makeRGB(shadowColorHex) 120 | } 121 | 122 | @Serializable 123 | data class BadgeColor( 124 | val fontColorHex: String, 125 | val bgColorHex: String, 126 | ) { 127 | 128 | val fontColor: Int get() = Color.makeRGB(fontColorHex) 129 | val bgColor: Int get() = Color.makeRGB(bgColorHex) 130 | } 131 | 132 | val cardBgColor: Int get() = Color.makeRGB(cardBgColorHex) 133 | 134 | //val cardOutlineColor: Int get() = Color.makeRGB(cardOutlineColorHex) 135 | val cardOutlineColors: IntArray get() = cardOutlineColorHex.split(";").map { Color.makeRGB(it) }.toIntArray() 136 | val faceOutlineColor: Int get() = Color.makeRGB(faceOutlineColorHex) 137 | val drawOutlineColor: Int get() = Color.makeRGB(drawOutlineColorHex) 138 | 139 | val nameColor: Int get() = Color.makeRGB(nameColorHex) 140 | val titleColor: Int get() = Color.makeRGB(titleColorHex) 141 | val subTitleColor: Int get() = Color.makeRGB(subTitleColorHex) 142 | val descColor: Int get() = Color.makeRGB(descColorHex) 143 | val contentColor: Int get() = Color.makeRGB(contentColorHex) 144 | val linkColor: Int get() = Color.makeRGB(linkColorHex) 145 | val footerColor: Int get() = Color.makeRGB(footerColorHex) 146 | 147 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Follow.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class IsFollow( 8 | //0:未关注 9 | //2:已关注 10 | //6:已互粉 11 | //128:拉黑 12 | @SerialName("attribute") 13 | val attribute: Int 14 | ) 15 | 16 | @Serializable 17 | data class FollowGroup( 18 | @SerialName("tagid") 19 | val tagId: Int, 20 | @SerialName("name") 21 | val name: String = "", 22 | @SerialName("count") 23 | val count: Int = 0 24 | ) -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/General.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * 统计 8 | */ 9 | @Serializable 10 | data class Stats( 11 | @SerialName("danmaku") 12 | val danmaku: Int = 0, 13 | @SerialName("dynamic") 14 | val dynamic: Int = 0, 15 | @SerialName("view") 16 | val view: Int, 17 | @SerialName("favorite") 18 | val favorite: Int, 19 | @SerialName("like") 20 | val like: Int, 21 | @SerialName("dislike") 22 | val dislike: Int, 23 | @SerialName("reply") 24 | val reply: Int, 25 | @SerialName("share") 26 | val share: Int, 27 | @SerialName("coin") 28 | val coin: Int, 29 | ) -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Live.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | data class LiveList( 9 | @SerialName("rooms") 10 | val rooms: List 11 | ) 12 | 13 | @Serializable 14 | data class LiveDetail( 15 | @SerialName("item") 16 | val item: LiveInfo, 17 | 18 | @SerialName("contact") 19 | val contact: String? = null 20 | ) 21 | 22 | @Serializable 23 | data class LiveInfo( 24 | @SerialName("title") 25 | val title: String, 26 | @SerialName("room_id") 27 | val roomId: Long, 28 | @SerialName("uid") 29 | val uid: Long, 30 | @SerialName("uname") 31 | val uname: String, 32 | @SerialName("face") 33 | val face: String, 34 | @SerialName("cover_from_user") 35 | val cover: String, 36 | @SerialName("liveTime") 37 | val liveTimeStart: Long? = null, 38 | @SerialName("live_time") 39 | val liveTimeDuration: Long, 40 | @SerialName("live_status") 41 | val liveStatus: Int, 42 | @SerialName("area_v2_name") 43 | val area: String, 44 | ){ 45 | val liveTime get() = liveTimeStart ?: liveTimeDuration 46 | } 47 | 48 | @Serializable 49 | data class LiveRoomDetail( 50 | @SerialName("uid") 51 | val uid: Long, 52 | @SerialName("room_id") 53 | val roomId: Long, 54 | @SerialName("short_id") 55 | val shortId: Int? = null, 56 | @SerialName("attention") 57 | val attention: Int? = null, 58 | @SerialName("online") 59 | val online: Int? = null, 60 | @SerialName("is_portrait") 61 | val isPortrait: Boolean? = null, 62 | @SerialName("description") 63 | val description: String? = null, 64 | @SerialName("live_status") 65 | val liveStatus: Int, 66 | @SerialName("area_id") 67 | val areaId: Int? = null, 68 | @SerialName("parent_area_id") 69 | val parentAreaId: Int? = null, 70 | @SerialName("parent_area_name") 71 | val parentAreaName: String? = null, 72 | @SerialName("old_area_id") 73 | val oldAreaId: Int? = null, 74 | @SerialName("background") 75 | val background: String? = null, 76 | @SerialName("title") 77 | val title: String, 78 | @SerialName("user_cover") 79 | val cover: String, 80 | @SerialName("keyframe") 81 | val keyframe: String? = null, 82 | @SerialName("is_strict_room") 83 | val isStrictRoom: Boolean? = null, 84 | @SerialName("live_time") 85 | val liveTime: String? = null, 86 | @SerialName("tags") 87 | val tags: String? = null, 88 | @SerialName("is_anchor") 89 | val isAnchor: Int? = null, 90 | @SerialName("room_silent_type") 91 | val roomSilentType: String? = null, 92 | @SerialName("room_silent_level") 93 | val roomSilentLevel: Int? = null, 94 | @SerialName("room_silent_second") 95 | val roomSilentSecond: Int? = null, 96 | @SerialName("area_name") 97 | val areaName: String? = null, 98 | @SerialName("pendants") 99 | val pendants: String? = null, 100 | @SerialName("area_pendants") 101 | val areaPendants: String? = null, 102 | @SerialName("hot_words") 103 | val hotWords: List? = null, 104 | @SerialName("hot_words_status") 105 | val hotWordsStatus: Int? = null, 106 | @SerialName("verify") 107 | val verify: String? = null, 108 | @SerialName("new_pendants") 109 | val newPendants: NewPendants? = null, 110 | @SerialName("up_session") 111 | val upSession: String? = null, 112 | @SerialName("pk_status") 113 | val pkStatus: Int? = null, 114 | @SerialName("pk_id") 115 | val pkId: Int? = null, 116 | @SerialName("battle_id") 117 | val battleId: Int? = null, 118 | @SerialName("allow_change_area_time") 119 | val allowChangeAreaTime: Int? = null, 120 | @SerialName("allow_upload_cover_time") 121 | val allowUploadCoverTime: Int? = null, 122 | ): BiliDetail{ 123 | @Serializable 124 | data class NewPendants( 125 | @SerialName("frame") 126 | val frame: Frame? = null, 127 | @SerialName("badge") 128 | val badge: Badge? = null, 129 | @SerialName("mobile_frame") 130 | val mobileFrame: MobileFrame? = null, 131 | @SerialName("mobile_badge") 132 | val mobileBadge: String? = null, 133 | ){ 134 | @Serializable 135 | data class Frame( 136 | @SerialName("name") 137 | val name: String? = null, 138 | @SerialName("value") 139 | val value: String? = null, 140 | @SerialName("position") 141 | val position: Int? = null, 142 | @SerialName("desc") 143 | val desc: String? = null, 144 | @SerialName("area") 145 | val area: Int? = null, 146 | @SerialName("area_old") 147 | val areaOld: Int? = null, 148 | @SerialName("bg_color") 149 | val bgColor: String? = null, 150 | @SerialName("bg_pic") 151 | val bgPic: String? = null, 152 | @SerialName("use_old_area") 153 | val useOldArea: Boolean? = null, 154 | ) 155 | @Serializable 156 | data class Badge( 157 | @SerialName("name") 158 | val name: String? = null, 159 | @SerialName("position") 160 | val position: Int? = null, 161 | @SerialName("value") 162 | val value: String? = null, 163 | @SerialName("desc") 164 | val desc: String? = null, 165 | ) 166 | @Serializable 167 | data class MobileFrame( 168 | @SerialName("name") 169 | val name: String? = null, 170 | @SerialName("value") 171 | val value: String? = null, 172 | @SerialName("position") 173 | val position: Int? = null, 174 | @SerialName("desc") 175 | val desc: String? = null, 176 | @SerialName("area") 177 | val area: Int? = null, 178 | @SerialName("area_old") 179 | val areaOld: Int? = null, 180 | @SerialName("bg_color") 181 | val bgColor: String? = null, 182 | @SerialName("bg_pic") 183 | val bgPic: String? = null, 184 | @SerialName("use_old_area") 185 | val useOldArea: Boolean? = null, 186 | ) 187 | } 188 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Login.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class LoginData( 8 | @SerialName("code") 9 | val code: Int? = null, 10 | @SerialName("message") 11 | val message: String? = null, 12 | @SerialName("refresh_token") 13 | val refreshToken: String? = null, 14 | @SerialName("timestamp") 15 | val timestamp: Long? = null, 16 | @SerialName("url") 17 | val url: String? = null, 18 | ) 19 | 20 | @Serializable 21 | data class LoginQrcode( 22 | @SerialName("url") 23 | val url: String, 24 | @SerialName("qrcode_key") 25 | val qrcodeKey: String? = null 26 | ) -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Result.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | 7 | @Serializable 8 | data class BiliResult( 9 | @SerialName("code") 10 | val code: Int, 11 | @SerialName("message") 12 | val message: String? = null, 13 | @SerialName("ttl") 14 | val ttl: Int? = null, 15 | @SerialName("data") 16 | val data: JsonElement? = null 17 | ) 18 | 19 | @Serializable 20 | data class PgcResult( 21 | @SerialName("code") 22 | val code: Int, 23 | @SerialName("message") 24 | val message: String? = null, 25 | @SerialName("result") 26 | val result: JsonElement? = null 27 | ) 28 | 29 | @Serializable 30 | data class ShortLinkData( 31 | @SerialName("title") 32 | val title: String? = null, 33 | @SerialName("content") 34 | val content: String? = null, 35 | @SerialName("link") 36 | val link: String, 37 | @SerialName("count") 38 | val count: Int? = null 39 | ) -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Video.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class VideoDetail( 8 | @SerialName("bvid") 9 | val bvid: String, 10 | @SerialName("aid") 11 | val aid: String, 12 | @SerialName("videos") 13 | val videos: Int? = null, 14 | @SerialName("tid") 15 | val tid: Int? = null, 16 | @SerialName("tname") 17 | val tname: String? = null, 18 | @SerialName("copyright") 19 | val copyright: Int? = null, 20 | @SerialName("pic") 21 | val pic: String, 22 | @SerialName("title") 23 | val title: String, 24 | @SerialName("pubdate") 25 | val pubdate: Long, 26 | @SerialName("ctime") 27 | val ctime: Long, 28 | @SerialName("desc") 29 | val desc: String, 30 | @SerialName("state") 31 | val state: Int? = null, 32 | @SerialName("duration") 33 | val duration: Long, 34 | @SerialName("mission_id") 35 | val missionId: Int? = null, 36 | @SerialName("rights") 37 | val rights: Rights? = null, 38 | @SerialName("owner") 39 | val owner: Owner, 40 | @SerialName("stat") 41 | val stat: Stat, 42 | @SerialName("dynamic") 43 | val dynamic: String? = null, 44 | @SerialName("cid") 45 | val cid: Long? = null, 46 | @SerialName("dimension") 47 | val dimension: Dimension? = null, 48 | @SerialName("season_id") 49 | val seasonId: Int? = null, 50 | @SerialName("premiere") 51 | val premiere: Premiere? = null, 52 | @SerialName("teenage_mode") 53 | val teenageMode: Int? = null, 54 | @SerialName("is_chargeable_season") 55 | val isChargeableSeason: Boolean? = null, 56 | @SerialName("is_story") 57 | val isStory: Boolean? = null, 58 | @SerialName("no_cache") 59 | val noCache: Boolean? = null, 60 | @SerialName("pages") 61 | val pages: List? = null, 62 | @SerialName("ugc_season") 63 | val ugcSeason: UgcSeason? = null, 64 | @SerialName("is_season_display") 65 | val isSeasonDisplay: Boolean? = null, 66 | @SerialName("like_icon") 67 | val likeIcon: String? = null, 68 | ): BiliDetail{ 69 | @Serializable 70 | data class Rights( 71 | @SerialName("bp") 72 | val bp: Int? = null, 73 | @SerialName("elec") 74 | val elec: Int? = null, 75 | @SerialName("download") 76 | val download: Int? = null, 77 | @SerialName("movie") 78 | val movie: Int? = null, 79 | @SerialName("pay") 80 | val pay: Int? = null, 81 | @SerialName("hd5") 82 | val hd5: Int? = null, 83 | @SerialName("no_reprint") 84 | val noReprint: Int? = null, 85 | @SerialName("autoplay") 86 | val autoplay: Int? = null, 87 | @SerialName("ugc_pay") 88 | val ugcPay: Int? = null, 89 | @SerialName("is_cooperation") 90 | val isCooperation: Int? = null, 91 | @SerialName("ugc_pay_preview") 92 | val ugcPayPreview: Int? = null, 93 | @SerialName("no_background") 94 | val noBackground: Int? = null, 95 | @SerialName("clean_mode") 96 | val cleanMode: Int? = null, 97 | @SerialName("is_stein_gate") 98 | val isSteinGate: Int? = null, 99 | @SerialName("is_360") 100 | val is360: Int? = null, 101 | @SerialName("no_share") 102 | val noShare: Int? = null, 103 | @SerialName("arc_pay") 104 | val arcPay: Int? = null, 105 | @SerialName("free_watch") 106 | val freeWatch: Int? = null, 107 | ) 108 | @Serializable 109 | data class Owner( 110 | @SerialName("mid") 111 | val mid: Long, 112 | @SerialName("name") 113 | val name: String, 114 | @SerialName("face") 115 | val face: String, 116 | ) 117 | @Serializable 118 | data class Stat( 119 | @SerialName("aid") 120 | val aid: Long? = null, 121 | @SerialName("view") 122 | val view: Int, 123 | @SerialName("danmaku") 124 | val danmaku: Int, 125 | @SerialName("reply") 126 | val reply: Int? = null, 127 | @SerialName("favorite") 128 | val favorite: Int? = null, 129 | @SerialName("coin") 130 | val coin: Int? = null, 131 | @SerialName("share") 132 | val share: Int? = null, 133 | @SerialName("now_rank") 134 | val nowRank: Int? = null, 135 | @SerialName("his_rank") 136 | val hisRank: Int? = null, 137 | @SerialName("like") 138 | val like: Int? = null, 139 | @SerialName("dislike") 140 | val dislike: Int? = null, 141 | @SerialName("evaluation") 142 | val evaluation: String? = null, 143 | @SerialName("argue_msg") 144 | val argueMsg: String? = null, 145 | ) 146 | @Serializable 147 | data class Dimension( 148 | @SerialName("width") 149 | val width: Int? = null, 150 | @SerialName("height") 151 | val height: Int? = null, 152 | @SerialName("rotate") 153 | val rotate: Int? = null, 154 | ) 155 | @Serializable 156 | data class Premiere( 157 | @SerialName("state") 158 | val state: Int? = null, 159 | @SerialName("start_time") 160 | val startTime: Long? = null, 161 | @SerialName("now_time") 162 | val nowTime: Long? = null, 163 | @SerialName("room_id") 164 | val roomId: Long? = null, 165 | @SerialName("sid") 166 | val sid: Int? = null, 167 | ) 168 | @Serializable 169 | data class Pages( 170 | @SerialName("cid") 171 | val cid: Long? = null, 172 | @SerialName("page") 173 | val page: Int? = null, 174 | @SerialName("from") 175 | val from: String? = null, 176 | @SerialName("part") 177 | val part: String? = null, 178 | @SerialName("duration") 179 | val duration: Int? = null, 180 | @SerialName("vid") 181 | val vid: String? = null, 182 | @SerialName("weblink") 183 | val weblink: String? = null, 184 | @SerialName("dimension") 185 | val dimension: Dimension? = null, 186 | @SerialName("first_frame") 187 | val firstFrame: String? = null, 188 | ) 189 | 190 | @Serializable 191 | data class UgcSeason( 192 | @SerialName("id") 193 | val id: Long? = null, 194 | @SerialName("title") 195 | val title: String? = null, 196 | @SerialName("cover") 197 | val cover: String? = null, 198 | @SerialName("mid") 199 | val mid: Long? = null, 200 | @SerialName("intro") 201 | val intro: String? = null, 202 | @SerialName("sign_state") 203 | val signState: Int? = null, 204 | @SerialName("attribute") 205 | val attribute: Int? = null, 206 | @SerialName("ep_count") 207 | val epCount: Int? = null, 208 | @SerialName("season_type") 209 | val seasonType: Int? = null, 210 | @SerialName("is_pay_season") 211 | val isPaySeason: Boolean? = null, 212 | ) 213 | } 214 | 215 | @Serializable 216 | data class VideoList( 217 | val list: VList 218 | ){ 219 | @Serializable 220 | data class VList( 221 | val vlist: List 222 | ) 223 | } 224 | 225 | @Serializable 226 | data class VideoInfo( 227 | val aid: Long, 228 | val title: String, 229 | val bvid: String, 230 | val description: String, 231 | val pic: String, 232 | val length: String, 233 | @SerialName("created") 234 | val time: Long, 235 | val mid: Long, 236 | val author: String, 237 | val play: Long, 238 | @SerialName("video_review") 239 | val danmaku: Long, 240 | ) 241 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/data/Vote.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Vote( 8 | @SerialName("info") 9 | val info: VoteInfo, 10 | ) { 11 | @Serializable 12 | data class VoteInfo( 13 | @SerialName("vote_id") 14 | val voteId: Long, 15 | ) 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/draw/LiveDraw.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.draw 2 | 3 | import org.jetbrains.skia.* 4 | import org.jetbrains.skia.paragraph.Alignment 5 | import org.jetbrains.skia.paragraph.ParagraphBuilder 6 | import org.jetbrains.skia.paragraph.ParagraphStyle 7 | import org.jetbrains.skia.svg.SVGDOM 8 | import top.colter.mirai.plugin.bilibili.BiliConfig 9 | import top.colter.mirai.plugin.bilibili.BiliData 10 | import top.colter.mirai.plugin.bilibili.data.LiveInfo 11 | import top.colter.mirai.plugin.bilibili.utils.* 12 | import kotlin.math.abs 13 | 14 | 15 | suspend fun LiveInfo.makeDrawLive(colors: List): String { 16 | val live = drawLive() 17 | val img = makeCardBg(live.height, colors) { 18 | it.drawImage(live, 0f, 0f) 19 | } 20 | return cacheImage(img, "$uid/${liveTime.formatTime("yyyyMMddHHmmss")}.png", CacheType.DRAW_LIVE) 21 | } 22 | 23 | suspend fun LiveInfo.drawLive(): Image { 24 | val margin = quality.cardMargin * 2 25 | 26 | val avatar = drawAvatar() 27 | val fw = cardRect.width - quality.cardOutlineWidth / 2 28 | val fallbackUrl = imgApi(cover, fw.toInt(), (fw * 0.625).toInt()) 29 | val cover = getOrDownloadImageDefault(cover, fallbackUrl, CacheType.IMAGES) 30 | 31 | val height = (avatar.height + quality.contentSpace + cover.height * cardRect.width / cover.width).toInt() 32 | 33 | val footerTemplate = BiliConfig.templateConfig.footer.liveFooter 34 | val footerParagraph = if (footerTemplate.isNotBlank()) { 35 | val footer = footerTemplate 36 | .replace("{name}", uname) 37 | .replace("{uid}", uid.toString()) 38 | .replace("{id}", roomId.toString()) 39 | .replace("{time}", liveTime.formatTime) 40 | .replace("{type}", "直播") 41 | ParagraphBuilder(footerParagraphStyle, FontUtils.fonts).addText(footer).build().layout(cardRect.width) 42 | } else null 43 | 44 | return Surface.makeRasterN32Premul( 45 | (cardRect.width + margin).toInt(), 46 | height + quality.badgeHeight + margin + (footerParagraph?.height?.toInt() ?: 0) 47 | ).apply { 48 | canvas.apply { 49 | 50 | val rrect = RRect.makeComplexXYWH( 51 | margin / 2f, 52 | quality.badgeHeight + margin / 2f, 53 | cardRect.width, 54 | height.toFloat(), 55 | cardBadgeArc 56 | ) 57 | 58 | drawRectShadowAntiAlias(rrect.inflate(1f), theme.cardShadow) 59 | 60 | if (BiliConfig.imageConfig.badgeEnable.left) { 61 | val svg = SVGDOM(Data.makeFromBytes(loadResourceBytes("icon/LIVE.svg"))) 62 | drawBadge( 63 | "直播", 64 | font, 65 | theme.mainLeftBadge.fontColor, 66 | theme.mainLeftBadge.bgColor, 67 | rrect, 68 | Position.TOP_LEFT, 69 | svg.makeImage(quality.contentFontSize, quality.contentFontSize) 70 | ) 71 | } 72 | if (BiliConfig.imageConfig.badgeEnable.right) { 73 | drawBadge(roomId.toString(), font, Color.WHITE, Color.makeRGB(72, 199, 240), rrect, Position.TOP_RIGHT) 74 | } 75 | 76 | drawCard(rrect) 77 | 78 | var top = quality.cardMargin + quality.badgeHeight.toFloat() 79 | 80 | drawScaleWidthImage(avatar, cardRect.width, quality.cardMargin.toFloat(), top) 81 | top += avatar.height + quality.contentSpace 82 | 83 | val dst = RRect.makeXYWH( 84 | quality.cardMargin.toFloat(), 85 | top, 86 | cardRect.width - quality.cardOutlineWidth / 2, 87 | (cardRect.width * cover.height / cover.width) - quality.cardOutlineWidth / 2, 88 | quality.cardArc 89 | ) 90 | drawImageRRect(cover, dst) 91 | 92 | footerParagraph?.paint(this, cardRect.left, rrect.bottom + quality.cardMargin / 2) 93 | 94 | } 95 | }.makeImageSnapshot() 96 | } 97 | 98 | suspend fun LiveInfo.drawAvatar(): Image { 99 | return Surface.makeRasterN32Premul( 100 | cardRect.width.toInt(), 101 | (quality.faceSize + quality.cardPadding * 2f).toInt() 102 | ).apply surface@{ 103 | canvas.apply { 104 | drawAvatar(face, null, null, quality.faceSize, quality.verifyIconSize) 105 | 106 | val paragraphStyle = ParagraphStyle().apply { 107 | maxLinesCount = 1 108 | ellipsis = "..." 109 | alignment = Alignment.LEFT 110 | textStyle = titleTextStyle.apply { 111 | fontSize = quality.nameFontSize 112 | } 113 | } 114 | 115 | val w = cardContentRect.width - quality.pendantSize - 116 | if (BiliConfig.imageConfig.cardOrnament == "QrCode" ) quality.ornamentHeight else 0f 117 | 118 | val titleParagraph = 119 | ParagraphBuilder(paragraphStyle, FontUtils.fonts).addText(title).build() 120 | .layout(w) 121 | paragraphStyle.apply { 122 | textStyle = descTextStyle.apply { 123 | fontSize = quality.subTitleFontSize 124 | } 125 | } 126 | val timeParagraph = 127 | ParagraphBuilder(paragraphStyle, FontUtils.fonts).addText("$uname ${liveTime.formatTime}").build() 128 | .layout(w) 129 | 130 | val x = quality.faceSize + quality.cardPadding * 3f 131 | val space = (quality.pendantSize - quality.nameFontSize - quality.subTitleFontSize) / 3 132 | var y = space * 1.25f 133 | 134 | titleParagraph.paint(this, x, y) 135 | 136 | y += quality.nameFontSize + space * 0.5f 137 | timeParagraph.paint(this, x, y) 138 | 139 | val color = BiliData.dynamic[uid]?.color ?: BiliConfig.imageConfig.defaultColor 140 | val colors = color.split(";", ";").map { Color.makeRGB(it.trim()) }.first() 141 | drawLiveOrnament("https://live.bilibili.com/$roomId", colors, area) 142 | } 143 | }.makeImageSnapshot() 144 | } 145 | 146 | fun Canvas.drawLiveOrnament(link: String?, qrCodeColor: Int?, label: String?) { 147 | when (BiliConfig.imageConfig.cardOrnament) { 148 | "QrCode" -> { 149 | val qrCodeImg = qrCode(link!!, quality.ornamentHeight.toInt(), qrCodeColor!!) 150 | val y = ((quality.faceSize - qrCodeImg.height + quality.contentSpace) / 2) 151 | val tarFRect = Rect.makeXYWH( 152 | cardRect.width - qrCodeImg.width - abs(y), 153 | y + quality.cardPadding, 154 | qrCodeImg.width.toFloat(), 155 | qrCodeImg.height.toFloat() 156 | ) 157 | 158 | val srcFRect = Rect.makeXYWH(0f, 0f, qrCodeImg.width.toFloat(), qrCodeImg.height.toFloat()) 159 | drawImageRect( 160 | qrCodeImg, 161 | srcFRect, 162 | tarFRect, 163 | FilterMipmap(FilterMode.LINEAR, MipmapMode.NEAREST), 164 | Paint(), 165 | true 166 | ) 167 | } 168 | "None" -> {} 169 | else -> { 170 | //val labelTextLine = TextLine.make(label, font.makeWithSize(quality.subTitleFontSize)) 171 | //val y = 172 | // ((quality.faceSize - quality.subTitleFontSize - quality.badgePadding * 2 + quality.contentSpace) / 2) 173 | //drawLabelCard( 174 | // labelTextLine, 175 | // cardContentRect.right - labelTextLine.width - quality.badgePadding * 4 - abs(y), 176 | // y + quality.cardPadding, 177 | // Paint().apply { 178 | // color = theme.subLeftBadge.fontColor 179 | // }, 180 | // Paint().apply { 181 | // color = theme.subLeftBadge.bgColor 182 | // } 183 | //) 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/draw/QrCodeDraw.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.draw 2 | 3 | import com.google.zxing.BarcodeFormat 4 | import com.google.zxing.EncodeHintType 5 | import com.google.zxing.client.j2se.MatrixToImageConfig 6 | import com.google.zxing.client.j2se.MatrixToImageWriter 7 | import com.google.zxing.qrcode.QRCodeWriter 8 | import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel 9 | import org.jetbrains.skia.* 10 | import org.jetbrains.skia.svg.SVGDOM 11 | import org.jetbrains.skiko.toBitmap 12 | import top.colter.mirai.plugin.bilibili.utils.loadResourceBytes 13 | 14 | val pointColor = 0xFF000000 15 | val bgColor = 0xFFFFFFFF 16 | 17 | fun loginQrCode(url: String): Image { 18 | val qrCodeWriter = QRCodeWriter() 19 | 20 | val bitMatrix = qrCodeWriter.encode( 21 | url, BarcodeFormat.QR_CODE, 250, 250, 22 | mapOf( 23 | EncodeHintType.MARGIN to 1, 24 | EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H 25 | ) 26 | ) 27 | 28 | val config = MatrixToImageConfig(pointColor.toInt(), bgColor.toInt()) 29 | 30 | return Surface.makeRasterN32Premul(250, 250).apply { 31 | canvas.apply { 32 | 33 | val img = Image.makeFromBitmap(MatrixToImageWriter.toBufferedImage(bitMatrix, config).toBitmap()) 34 | drawImage(img, 0f, 0f) 35 | 36 | drawCircle(125f, 125f, 35f, Paint().apply { 37 | color = Color.WHITE 38 | }) 39 | drawCircle(125f, 125f, 30f, Paint().apply { 40 | color = Color.makeRGB(2, 181, 218) 41 | }) 42 | 43 | val svg = SVGDOM(Data.makeFromBytes(loadResourceBytes("icon/BILIBILI_LOGO.svg"))) 44 | drawImage(svg.makeImage(40f, 40f), 105f, 105f, Paint().apply { 45 | colorFilter = ColorFilter.makeBlend(Color.WHITE, BlendMode.SRC_ATOP) 46 | }) 47 | 48 | } 49 | }.makeImageSnapshot() 50 | } 51 | 52 | 53 | fun qrCode(url: String, width: Int, color: Int): Image { 54 | val qrCodeWriter = QRCodeWriter() 55 | 56 | val bitMatrix = qrCodeWriter.encode( 57 | url, BarcodeFormat.QR_CODE, width, width, 58 | mapOf( 59 | EncodeHintType.MARGIN to 0 60 | ) 61 | ) 62 | 63 | val c = Color.getRGB(color) 64 | val cc = c[0] + c[1] + c[2] 65 | val ccc = if (cc > 382) { 66 | val hsb = rgb2hsb(c[0], c[1], c[2]) 67 | hsb[1] = if (hsb[1] + 0.25f > 1f) 1f else hsb[1] + 0.25f 68 | val rgb = hsb2rgb(hsb[0], hsb[1], hsb[2]) 69 | Color.makeRGB(rgb[0], rgb[1], rgb[2]) 70 | } else { 71 | color 72 | } 73 | 74 | val config = MatrixToImageConfig(ccc, Color.makeARGB(0, 255, 255, 255)) 75 | 76 | return Image.makeFromBitmap(MatrixToImageWriter.toBufferedImage(bitMatrix, config).toBitmap()) 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/old/BiliPluginConfig.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.old 2 | 3 | import net.mamoe.mirai.console.data.AutoSavePluginConfig 4 | import net.mamoe.mirai.console.data.ValueDescription 5 | import net.mamoe.mirai.console.data.value 6 | 7 | /** 8 | * v2版配置,用于数据迁移 9 | */ 10 | object BiliPluginConfig : AutoSavePluginConfig("BiliPluginConfig") { 11 | 12 | @ValueDescription("数据是否迁移") 13 | var migrated: Boolean by value(false) 14 | 15 | @ValueDescription("管理员") 16 | val admin: String by value("") 17 | 18 | @ValueDescription("推送模式\n0: 文字推送\n1: 图片推送") 19 | val pushMode: Int by value(1) 20 | 21 | @ValueDescription("添加订阅时是否允许 bot 自动关注未关注的用户") 22 | val autoFollow: Boolean by value(true) 23 | 24 | @ValueDescription("Bot 关注时保存的分组(最长16字符)") 25 | val followGroup: String by value("Bot关注") 26 | 27 | @ValueDescription("检测间隔(推荐 15-30) 单位秒") 28 | val interval: Int by value(15) 29 | 30 | @ValueDescription("直播检测间隔(与动态检测独立) 单位秒") 31 | val liveInterval: Int by value(20) 32 | 33 | @ValueDescription("低频检测时间段与倍率(例: 3-8x2 三点到八点检测间隔为正常间隔的2倍) 24小时制") 34 | val lowSpeed: String by value("0-0x2") 35 | 36 | @ValueDescription("图片推送模式用的字体, 详细请看 readme") 37 | val font: String by value("") 38 | 39 | @ValueDescription("动态/视频推送文字模板, 参数请看 readme") 40 | val pushTemplate: String by value("{name}@{type}\n{link}") 41 | 42 | @ValueDescription("直播推送文字模板, 如不配置则与上面的动态推送模板一致") 43 | val livePushTemplate: String by value("") 44 | 45 | @ValueDescription("页脚模板") 46 | val footerTemplate: String by value("{type}ID: {id}") 47 | 48 | @ValueDescription("是否开启图片二维码") 49 | val qrCode: Boolean by value(false) 50 | 51 | @ValueDescription("卡片圆角大小") 52 | val cardArc: Int by value(20) 53 | 54 | //@Suppress(stringSerialization = DOUBLE_QUOTATION) 55 | @ValueDescription("cookie, 请使用双引号") 56 | var cookie: String by value("") 57 | 58 | @ValueDescription("百度翻译") 59 | val baiduTranslate: Map by value( 60 | mapOf( 61 | //是否开启百度翻译 62 | "enable" to "false", 63 | //百度翻译api密钥 64 | "APP_ID" to "", 65 | "SECURITY_KEY" to "" 66 | ) 67 | ) 68 | 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/old/BiliSubscribeData.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.old 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import net.mamoe.mirai.console.data.AutoSavePluginData 6 | import net.mamoe.mirai.console.data.ValueDescription 7 | import net.mamoe.mirai.console.data.value 8 | import java.time.Instant 9 | 10 | /** 11 | * v2版数据,用于数据迁移 12 | */ 13 | object BiliSubscribeData : AutoSavePluginData("BiliSubscribeData") { 14 | @ValueDescription("数据是否迁移") 15 | var migrated: Boolean by value(false) 16 | 17 | @ValueDescription("订阅信息") 18 | val dynamic: MutableMap by value(mutableMapOf(0L to SubDataOld("ALL"))) 19 | } 20 | 21 | @Serializable 22 | data class SubDataOld( 23 | @SerialName("name") 24 | val name: String, 25 | @SerialName("color") 26 | var color: String = "#d3edfa", 27 | @SerialName("last") 28 | var last: Long = Instant.now().epochSecond, 29 | @SerialName("lastLive") 30 | var lastLive: Long = Instant.now().epochSecond, 31 | @SerialName("contacts") 32 | val contacts: MutableMap = mutableMapOf(), 33 | @SerialName("banList") 34 | val banList: MutableMap = mutableMapOf(), 35 | @SerialName("filter") 36 | val filter: MutableMap> = mutableMapOf(), 37 | @SerialName("containFilter") 38 | val containFilter: MutableMap> = mutableMapOf() 39 | ) -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/old/DataMigration.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.old 2 | 3 | import top.colter.mirai.plugin.bilibili.* 4 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic.reload 5 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic.save 6 | import top.colter.mirai.plugin.bilibili.utils.delegate 7 | import top.colter.mirai.plugin.bilibili.utils.findContactAll 8 | 9 | fun migration() { 10 | migrationData() 11 | migrationConfig() 12 | } 13 | 14 | fun migrationData() { 15 | if (BiliBiliDynamic.dataFolder.resolve("BiliSubscribeData.yml").exists()) { 16 | BiliSubscribeData.reload() 17 | if (!BiliSubscribeData.migrated) { 18 | BiliBiliDynamic.logger.info("开始转移旧版数据...") 19 | 20 | BiliSubscribeData.dynamic.forEach { (t, u) -> 21 | if (!BiliData.dynamic.containsKey(t) || t == 0L) { 22 | BiliData.dynamic[t] = SubData( 23 | u.name, 24 | if (u.color == "#d3edfa") null else u.color, 25 | u.last, 26 | u.lastLive, 27 | u.contacts.keys.toMutableSet(), 28 | //mutableSetOf(), 29 | u.banList 30 | ) 31 | u.contacts.forEach { (c, l) -> 32 | if (l != "11") { 33 | when (l[0]) { 34 | '0' -> { 35 | if (!BiliData.filter.containsKey(c)) { 36 | BiliData.filter[c] = mutableMapOf() 37 | } 38 | BiliData.filter[c]!![t] = DynamicFilter( 39 | typeSelect = TypeFilter( 40 | FilterMode.BLACK_LIST, 41 | mutableListOf( 42 | DynamicFilterType.DYNAMIC, 43 | DynamicFilterType.FORWARD, 44 | DynamicFilterType.VIDEO, 45 | DynamicFilterType.ARTICLE, 46 | DynamicFilterType.MUSIC 47 | ) 48 | ) 49 | ) 50 | } 51 | 52 | '2' -> { 53 | if (!BiliData.filter.containsKey(c)) { 54 | BiliData.filter[c] = mutableMapOf() 55 | } 56 | BiliData.filter[c]!![t] = DynamicFilter( 57 | typeSelect = TypeFilter( 58 | FilterMode.WHITE_LIST, 59 | mutableListOf(DynamicFilterType.VIDEO) 60 | ) 61 | ) 62 | } 63 | } 64 | when (l[1]) { 65 | '0' -> { 66 | if (!BiliData.filter.containsKey(c)) { 67 | BiliData.filter[c] = mutableMapOf() 68 | } 69 | if (!BiliData.filter[c]!!.containsKey(t)) { 70 | BiliData.filter[c]!![t] = DynamicFilter() 71 | } 72 | if (BiliData.filter[c]!![t]!!.typeSelect.mode == FilterMode.BLACK_LIST) { 73 | BiliData.filter[c]!![t]!!.typeSelect.list.add(DynamicFilterType.LIVE) 74 | } 75 | } 76 | 77 | '1' -> { 78 | if (BiliData.filter[c]?.get(t)?.typeSelect?.mode == FilterMode.WHITE_LIST) { 79 | BiliData.filter[c]?.get(t)?.typeSelect?.list?.add(DynamicFilterType.LIVE) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } else { 86 | BiliBiliDynamic.logger.warning("新旧数据冲突! $t") 87 | } 88 | } 89 | BiliData.save() 90 | BiliSubscribeData.migrated = true 91 | BiliSubscribeData.save() 92 | BiliBiliDynamic.logger.info("数据转移成功") 93 | } 94 | } 95 | } 96 | 97 | fun migrationConfig() { 98 | if (BiliBiliDynamic.configFolder.resolve("BiliPluginConfig.yml").exists()) { 99 | BiliPluginConfig.reload() 100 | if (!BiliPluginConfig.migrated) { 101 | BiliBiliDynamic.logger.info("开始转移旧版配置...") 102 | 103 | //BiliConfig.admin = BiliPluginConfig.admin.toLong() 104 | BiliConfig.accountConfig.cookie = BiliPluginConfig.cookie 105 | BiliConfig.accountConfig.autoFollow = BiliPluginConfig.autoFollow 106 | BiliConfig.accountConfig.followGroup = BiliPluginConfig.followGroup 107 | BiliConfig.checkConfig.interval = BiliPluginConfig.interval 108 | BiliConfig.checkConfig.liveInterval = BiliPluginConfig.liveInterval 109 | BiliConfig.checkConfig.lowSpeed = BiliPluginConfig.lowSpeed 110 | BiliConfig.imageConfig.font = BiliPluginConfig.font.split(";").first().split(".").first() 111 | //BiliConfig.templateConfig.dynamicPush["OldCustom"] = BiliPluginConfig.pushTemplate 112 | //BiliConfig.templateConfig.livePush["OldCustom"] = BiliPluginConfig.livePushTemplate 113 | BiliConfig.templateConfig.footer.dynamicFooter = BiliPluginConfig.footerTemplate 114 | BiliConfig.templateConfig.footer.liveFooter = BiliPluginConfig.footerTemplate 115 | if (BiliPluginConfig.qrCode) BiliConfig.imageConfig.cardOrnament = "QrCode" 116 | BiliConfig.enableConfig.translateEnable = BiliPluginConfig.baiduTranslate["enable"].toBoolean() 117 | BiliConfig.translateConfig.baidu.APP_ID = BiliPluginConfig.baiduTranslate["APP_ID"] ?: "" 118 | BiliConfig.translateConfig.baidu.SECURITY_KEY = BiliPluginConfig.baiduTranslate["SECURITY_KEY"] ?: "" 119 | 120 | BiliConfig.save() 121 | BiliPluginConfig.migrated = true 122 | BiliPluginConfig.save() 123 | BiliBiliDynamic.logger.info("配置转移成功") 124 | } 125 | } 126 | } 127 | 128 | fun updateData() { 129 | if (BiliData.dataVersion == 0) { 130 | BiliData.dynamicPushTemplate.forEach { 131 | val s = it.value.map { 132 | println(it) 133 | findContactAll(it.toLong())?.delegate 134 | }.filterNotNull().toSet() 135 | it.value.clear() 136 | it.value.addAll(s) 137 | } 138 | BiliData.livePushTemplate.forEach { 139 | val s = it.value.map { 140 | findContactAll(it.toLong())?.delegate 141 | }.filterNotNull().toSet() 142 | it.value.clear() 143 | it.value.addAll(s) 144 | } 145 | BiliData.atAll.apply { 146 | val m = map { 147 | (findContactAll(it.key.toLong())?.delegate?:it.key) to it.value 148 | }.toMap() 149 | clear() 150 | putAll(m) 151 | } 152 | BiliData.dataVersion = 1 153 | } 154 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/AtAllService.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | import net.mamoe.mirai.contact.Group 6 | import top.colter.mirai.plugin.bilibili.AtAllType 7 | import top.colter.mirai.plugin.bilibili.command.GroupOrContact 8 | import top.colter.mirai.plugin.bilibili.command.subject 9 | 10 | object AtAllService { 11 | private val mutex = Mutex() 12 | 13 | private fun toAtAllType(type: String) = 14 | when (type.lowercase()) { 15 | "全部", "all", "a" -> AtAllType.ALL 16 | "全部动态", "dynamic", "d" -> AtAllType.DYNAMIC 17 | "直播", "live", "l" -> AtAllType.LIVE 18 | "视频", "video", "v" -> AtAllType.VIDEO 19 | "音乐", "music", "m" -> AtAllType.MUSIC 20 | "专栏", "article" -> AtAllType.ARTICLE 21 | else -> null 22 | } 23 | 24 | suspend fun addAtAll(type: String, uid: Long = 0L, target: GroupOrContact) = mutex.withLock { 25 | val atAllType = toAtAllType(type) ?: return "没有这个类型哦 [$type]" 26 | if (target.group == null) { 27 | if (target.contact !is Group) return "仅在群聊中有用哦" 28 | if (target.contact.botPermission.level == 0) return "Bot不为管理员, 无法使用At全体" 29 | } 30 | val list = atAll.getOrPut(target.subject) { mutableMapOf() }.getOrPut(uid) { mutableSetOf() } 31 | if (list.isEmpty()) { 32 | list.add(atAllType) 33 | atAll[target.subject]?.set(uid, list) 34 | } else when (atAllType) { 35 | AtAllType.ALL -> { 36 | list.clear() 37 | list.add(atAllType) 38 | } 39 | AtAllType.DYNAMIC -> { 40 | list.removeAll(listOf(AtAllType.ALL, AtAllType.VIDEO, AtAllType.MUSIC, AtAllType.ARTICLE)) 41 | list.add(atAllType) 42 | } 43 | AtAllType.LIVE -> { 44 | list.remove(AtAllType.ALL) 45 | list.add(atAllType) 46 | } 47 | else -> { 48 | list.remove(AtAllType.ALL) 49 | list.remove(AtAllType.DYNAMIC) 50 | list.add(atAllType) 51 | } 52 | } 53 | "添加成功" 54 | } 55 | 56 | suspend fun delAtAll(type: String, uid: Long = 0L, subject: String) = mutex.withLock { 57 | val atAllType = toAtAllType(type) ?: return@withLock "没有这个类型哦 [$type]" 58 | if (atAll[subject]?.get(uid)?.remove(atAllType) == true) "删除成功" else "删除失败" 59 | } 60 | 61 | suspend fun listAtAll(uid: Long = 0L, subject: String) = mutex.withLock { 62 | val list = atAll[subject]?.get(uid) 63 | if (list.isNullOrEmpty()) return@withLock "没有At全体项哦" 64 | buildString { list.forEach { appendLine(it.value) } } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/FilterService.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | import top.colter.mirai.plugin.bilibili.DynamicFilter 6 | import top.colter.mirai.plugin.bilibili.DynamicFilterType 7 | import top.colter.mirai.plugin.bilibili.FilterMode 8 | import top.colter.mirai.plugin.bilibili.FilterType 9 | 10 | object FilterService { 11 | private val mutex = Mutex() 12 | 13 | suspend fun addFilter(type: FilterType, mode: FilterMode?, regex: String?, uid: Long, subject: String) = 14 | mutex.withLock { 15 | if (!isFollow(uid, subject)) return@withLock "还未订阅此人哦" 16 | 17 | if (!filter.containsKey(subject)) filter[subject] = mutableMapOf() 18 | if (!filter[subject]!!.containsKey(uid)) filter[subject]!![uid] = DynamicFilter() 19 | 20 | val dynamicFilter = filter[subject]!![uid]!! 21 | when (type) { 22 | FilterType.TYPE -> { 23 | if (mode != null) dynamicFilter.typeSelect.mode = mode 24 | if (regex != null && regex != "") { 25 | val t = when (regex) { 26 | "动态" -> DynamicFilterType.DYNAMIC 27 | "转发动态" -> DynamicFilterType.FORWARD 28 | "视频" -> DynamicFilterType.VIDEO 29 | "音乐" -> DynamicFilterType.MUSIC 30 | "专栏" -> DynamicFilterType.ARTICLE 31 | "直播" -> DynamicFilterType.LIVE 32 | else -> return@withLock "没有这个类型 $regex" 33 | } 34 | dynamicFilter.typeSelect.list.add(t) 35 | } 36 | } 37 | FilterType.REGULAR -> { 38 | if (mode != null) dynamicFilter.regularSelect.mode = mode 39 | if (regex != null && regex != "") dynamicFilter.regularSelect.list.add(regex) 40 | } 41 | } 42 | "设置成功" 43 | } 44 | 45 | suspend fun listFilter(uid: Long, subject: String) = mutex.withLock { 46 | if (!isFollow(uid, subject)) return@withLock "还未订阅此人哦" 47 | 48 | if (!(filter.containsKey(subject) && filter[subject]!!.containsKey(uid))) return@withLock "目标没有过滤器" 49 | 50 | buildString { 51 | //appendLine("当前目标过滤器: ") 52 | //appendLine() 53 | val typeSelect = filter[subject]!![uid]!!.typeSelect 54 | if (typeSelect.list.isNotEmpty()) { 55 | append("动态类型过滤器: ") 56 | appendLine(typeSelect.mode.value) 57 | typeSelect.list.forEachIndexed { index, type -> appendLine(" t$index: ${type.value}") } 58 | appendLine() 59 | } 60 | val regularSelect = filter[subject]!![uid]!!.regularSelect 61 | if (regularSelect.list.isNotEmpty()) { 62 | append("正则过滤器: ") 63 | appendLine(regularSelect.mode.value) 64 | regularSelect.list.forEachIndexed { index, reg -> appendLine(" r$index: $reg") } 65 | appendLine() 66 | } 67 | } 68 | } 69 | 70 | suspend fun delFilter(index: String, uid: Long, subject: String) = mutex.withLock { 71 | if (!isFollow(uid, subject)) return@withLock "还未订阅此人哦" 72 | if (!(filter.containsKey(subject) && filter[subject]!!.containsKey(uid))) return@withLock "当前目标没有过滤器" 73 | 74 | var i = 0 75 | runCatching { 76 | i = index.substring(1).toInt() 77 | }.onFailure { 78 | return@withLock "索引错误" 79 | } 80 | var flag = false 81 | val filter = if (index[0] == 't') { 82 | flag = true 83 | filter[subject]!![uid]!!.typeSelect.list 84 | } else if (index[0] == 'r') { 85 | filter[subject]!![uid]!!.regularSelect.list 86 | } else return@withLock "索引类型错误" 87 | if (filter.size < i) return@withLock "索引超出范围" 88 | val t = filter[i] 89 | filter.removeAt(i) 90 | 91 | if (flag) "已删除 ${(t as DynamicFilterType).value} 类型过滤" 92 | else "已删除 ${(t as String)} 正则过滤" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/General.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import net.mamoe.mirai.event.MessageSelectBuilder 4 | import net.mamoe.mirai.event.events.MessageEvent 5 | import net.mamoe.mirai.event.whileSelectMessages 6 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 7 | import top.colter.mirai.plugin.bilibili.BiliData 8 | import top.colter.mirai.plugin.bilibili.client.BiliClient 9 | 10 | internal val logger by BiliBiliDynamic::logger 11 | 12 | val client = BiliClient() 13 | 14 | val dynamic by BiliData::dynamic 15 | val filter by BiliData::filter 16 | val group by BiliData::group 17 | val atAll by BiliData::atAll 18 | val bangumi by BiliData::bangumi 19 | 20 | 21 | fun isFollow(uid: Long, subject: String) = 22 | uid == 0L || (dynamic.containsKey(uid) && dynamic[uid]!!.contacts.contains(subject)) 23 | 24 | 25 | suspend inline fun T.whileSelect( 26 | count: Int = 2, 27 | timeout: Long = 120_000, 28 | defaultReply: String = "没有这个选项哦", 29 | crossinline selectBuilder: MessageSelectBuilder.() -> Unit 30 | ): String? { 31 | var c = 0 32 | var res: String? = null 33 | whileSelectMessages { 34 | "退出" { 35 | subject.sendMessage("已退出") 36 | res = "退出" 37 | false 38 | } 39 | apply(selectBuilder) 40 | default { 41 | c++ 42 | subject.sendMessage("$defaultReply${if (c < count) ", 请重新输入" else ", 超出重试次数, 退出"}") 43 | if (c >= count) res = "超次" 44 | c < count 45 | } 46 | timeout(timeout) { 47 | res = "超时" 48 | false 49 | } 50 | } 51 | return res 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/GroupService.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | import net.mamoe.mirai.contact.Friend 6 | import top.colter.mirai.plugin.bilibili.BiliConfig 7 | import top.colter.mirai.plugin.bilibili.BiliData 8 | import top.colter.mirai.plugin.bilibili.Group 9 | import top.colter.mirai.plugin.bilibili.utils.delegate 10 | import top.colter.mirai.plugin.bilibili.utils.findContactAll 11 | import top.colter.mirai.plugin.bilibili.utils.name 12 | 13 | object GroupService { 14 | private val mutex = Mutex() 15 | 16 | suspend fun createGroup(name: String, operator: Long) = mutex.withLock { 17 | if (!group.containsKey(name)) { 18 | if (name.matches("^[0-9]*$".toRegex())) return@withLock "分组名不能全为数字" 19 | group[name] = Group(name, operator) 20 | "创建成功" 21 | }else "分组名称重复" 22 | } 23 | 24 | suspend fun delGroup(name: String, operator: Long) = mutex.withLock { 25 | if (group.containsKey(name)) { 26 | if (group[name]!!.creator == operator) { 27 | dynamic.forEach { (_, s) -> s.contacts.remove(name) } 28 | BiliData.dynamicPushTemplate.forEach { (_, c) -> c.remove(name) } 29 | BiliData.livePushTemplate.forEach { (_, c) -> c.remove(name) } 30 | filter.remove(name) 31 | atAll.remove(name) 32 | group.remove(name) 33 | "删除成功" 34 | }else "无权删除" 35 | }else "没有此分组 [$name]" 36 | } 37 | 38 | suspend fun listGroup(name: String? = null, operator: Long) = mutex.withLock { 39 | if (name == null) { 40 | group.values.filter { 41 | operator == BiliConfig.admin || operator == it.creator || it.admin.contains(operator) 42 | }.joinToString("\n") { 43 | "${it.name}@${findContactAll(it.creator)?.name?:it.creator}" 44 | }.ifEmpty { "没有创建或管理任何分组哦" } 45 | } else { 46 | group[name]?.toString() ?: "没有此分组哦" 47 | } 48 | } 49 | 50 | suspend fun setGroupAdmin(name: String, contacts: String, operator: Long) = mutex.withLock { 51 | if (group.containsKey(name)) { 52 | if (group[name]!!.creator == operator) { 53 | var failMsg = "" 54 | group[name]?.admin?.addAll(contacts.split(",",",").map { 55 | findContactAll(it).run { 56 | if (this != null && this is Friend) id else { 57 | failMsg += "$it, " 58 | null 59 | } 60 | } 61 | }.filterNotNull().toSet()) 62 | if (failMsg.isEmpty()) "添加成功" 63 | else "[$failMsg] 添加失败" 64 | }else "无权添加" 65 | }else "没有此分组 [$name]" 66 | } 67 | 68 | suspend fun banGroupAdmin(name: String, contacts: String, operator: Long) = mutex.withLock { 69 | if (group.containsKey(name)) { 70 | if (group[name]!!.creator == operator) { 71 | var failMsg = "" 72 | val admin = group[name]!!.admin 73 | contacts.split(",",",").map { 74 | try { 75 | it.toLong() 76 | }catch (e: NumberFormatException) { 77 | failMsg += "$it, " 78 | null 79 | } 80 | }.filterNotNull().toSet().forEach { 81 | if (!admin.remove(it)) failMsg += "$it, " 82 | } 83 | if (failMsg.isEmpty()) "删除成功" 84 | else "[$failMsg] 删除失败" 85 | }else "无权删除" 86 | }else "没有此分组 [$name]" 87 | } 88 | 89 | suspend fun pushGroupContact(name: String, contacts: String, operator: Long) = mutex.withLock { 90 | if (group.containsKey(name)) { 91 | if (checkGroupPerm(name, operator)) { 92 | var failMsg = "" 93 | group[name]?.contacts?.addAll(contacts.split(",",",").map { 94 | findContactAll(it)?.delegate.apply { 95 | if (this == null) failMsg += "$it, " 96 | } 97 | }.filterNotNull().toSet()) 98 | if (failMsg.isEmpty()) "添加成功" 99 | else "[$failMsg] 添加失败" 100 | }else "无权添加" 101 | }else "没有此分组 [$name]" 102 | } 103 | 104 | suspend fun delGroupContact(name: String, contacts: String, operator: Long) = mutex.withLock { 105 | if (group.containsKey(name)) { 106 | if (checkGroupPerm(name, operator)) { 107 | var failMsg = "" 108 | group[name]?.contacts?.removeAll(contacts.split(",",",").map { 109 | findContactAll(it)?.let { 110 | failMsg += "$it, " 111 | it.delegate 112 | } ?: "" 113 | }.filter { it.isNotEmpty() }.toSet()) 114 | if (failMsg.isEmpty()) "删除成功" 115 | else "[$failMsg] 删除失败" 116 | }else "无权删除" 117 | }else "没有此分组 [$name]" 118 | } 119 | 120 | fun checkGroupPerm(name: String, operator: Long): Boolean = 121 | group[name]?.creator == operator || group[name]?.admin?.contains(operator) == true 122 | 123 | } 124 | 125 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/LoginService.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.isActive 5 | import kotlinx.coroutines.withTimeout 6 | import net.mamoe.mirai.contact.Contact 7 | import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo 8 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 9 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 10 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic.save 11 | import top.colter.mirai.plugin.bilibili.BiliConfig 12 | import top.colter.mirai.plugin.bilibili.api.getLoginQrcode 13 | import top.colter.mirai.plugin.bilibili.api.loginInfo 14 | import top.colter.mirai.plugin.bilibili.draw.loginQrCode 15 | import top.colter.mirai.plugin.bilibili.initTagid 16 | import java.net.URI 17 | 18 | object LoginService { 19 | suspend fun login(contact: Contact) { 20 | val loginData = client.getLoginQrcode()!! 21 | 22 | val image = loginQrCode(loginData.url) 23 | val qrMsg = image.encodeToData()!!.bytes.toExternalResource().toAutoCloseable().sendAsImageTo(contact) 24 | val loginMsg = contact.sendMessage("请使用BiliBili手机APP扫码登录 3分钟有效") 25 | runCatching { 26 | withTimeout(180000) { 27 | while (isActive) { 28 | delay(3000) 29 | val loginInfo = client.loginInfo(loginData.qrcodeKey!!)!! 30 | if (loginInfo.code == 0) { 31 | val querys = URI(loginInfo.url!!).query.split("&") 32 | val cookie = buildString { 33 | querys.forEach { 34 | if (it.contains("SESSDATA") || it.contains("bili_jct")) 35 | append("${it.replace(",", "%2C").replace("*", "%2A")}; ") 36 | } 37 | } 38 | BiliConfig.accountConfig.cookie = cookie 39 | BiliConfig.save() 40 | BiliBiliDynamic.cookie.parse(cookie) 41 | initTagid() 42 | //getHistoryDynamic() 43 | contact.sendMessage("登录成功!") 44 | break 45 | } 46 | } 47 | } 48 | }.onFailure { 49 | contact.sendMessage("登录失败 ${it.message}") 50 | } 51 | try { 52 | qrMsg.recall() 53 | loginMsg.recall() 54 | }catch (_: Throwable) {} 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/PgcService.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import top.colter.mirai.plugin.bilibili.Bangumi 4 | import top.colter.mirai.plugin.bilibili.api.followPgc 5 | import top.colter.mirai.plugin.bilibili.api.getEpisodeInfo 6 | import top.colter.mirai.plugin.bilibili.api.getMediaInfo 7 | import top.colter.mirai.plugin.bilibili.api.getSeasonInfo 8 | 9 | val pgcRegex = """^((?:ss)|(?:md)|(?:ep))(\d{4,10})$""".toRegex() 10 | 11 | object PgcService { 12 | 13 | suspend fun followPgc(id: String, subject: String): String { 14 | val regex = pgcRegex.find(id) ?: return "ID 格式错误 例(ss11111, md22222, ep33333)" 15 | 16 | val type = regex.destructured.component1() 17 | val id = regex.destructured.component2().toLong() 18 | 19 | return when (type) { 20 | "ss" -> followPgcBySsid(id, subject) 21 | "md" -> followPgcByMdid(id, subject) 22 | "ep" -> followPgcByEpid(id, subject) 23 | else -> "额(⊙﹏⊙)" 24 | } 25 | } 26 | 27 | suspend fun followPgcBySsid(ssid: Long, subject: String): String { 28 | client.followPgc(ssid) ?: return "追番失败" 29 | bangumi.getOrPut(ssid) { 30 | val season = client.getSeasonInfo(ssid) ?: return "获取番剧信息失败, 如果是港澳台番剧请用 media id (md11111) 订阅" 31 | Bangumi(season.title, season.seasonId, season.mediaId, type(season.type)) 32 | }.apply { 33 | contacts.add(subject) 34 | return "追番成功( •̀ ω •́ )✧ [$title]" 35 | } 36 | } 37 | 38 | suspend fun followPgcByMdid(mdid: Long, subject: String): String { 39 | val season = client.getMediaInfo(mdid) ?: return "获取番剧信息失败" 40 | val ssid = season.media.seasonId 41 | client.followPgc(ssid) ?: return "追番失败" 42 | bangumi.getOrPut(ssid) { 43 | Bangumi(season.media.title, ssid, season.media.mediaId, season.media.typeName) 44 | }.apply { 45 | contacts.add(subject) 46 | return "追番成功( •̀ ω •́ )✧ [$title]" 47 | } 48 | } 49 | 50 | suspend fun followPgcByEpid(epid: Long, subject: String): String { 51 | val season = client.getEpisodeInfo(epid) ?: return "获取番剧信息失败, 如果是港澳台番剧请用 media id (md11111) 订阅" 52 | client.followPgc(season.seasonId) ?: return "追番失败" 53 | bangumi.getOrPut(season.seasonId) { 54 | Bangumi(season.title, season.seasonId, season.mediaId, type(season.type)) 55 | }.apply { 56 | contacts.add(subject) 57 | return "追番成功( •̀ ω •́ )✧ [$title]" 58 | } 59 | } 60 | 61 | fun delPgc(id: String, subject: String): String { 62 | val regex = pgcRegex.find(id) ?: return "ID 格式错误 例(ss11111, md22222)" 63 | 64 | val type = regex.destructured.component1() 65 | val id = regex.destructured.component2().toLong() 66 | 67 | return when (type) { 68 | "ss" -> { 69 | val pgc = bangumi[id] ?: return "没有这个番剧哦" 70 | if (pgc.contacts.remove(subject)) { 71 | if (pgc.contacts.isEmpty()) bangumi.remove(id) 72 | "删除成功" 73 | } else "没有订阅这个番剧哦" 74 | } 75 | "md" -> { 76 | val pgc = bangumi.filter { it.value.mediaId == id }.values 77 | if (pgc.isEmpty()) return "没有这个番剧哦" 78 | val contacts = pgc.first().contacts 79 | if (contacts.remove(subject)) { 80 | if (contacts.isEmpty()) bangumi.remove(pgc.first().seasonId) 81 | "删除成功" 82 | } else "没有订阅这个番剧哦" 83 | } 84 | "ep" -> "无法通过ep进行删除,请使用 ss 或 md" 85 | else -> "额(⊙﹏⊙)" 86 | } 87 | } 88 | 89 | fun type(type: Int) = when (type) { 90 | 1 -> "番剧" 91 | 2 -> "电影" 92 | 3 -> "纪录片" 93 | 4 -> "国创" 94 | 5 -> "电视剧" 95 | 7 -> "综艺" 96 | else -> "未知" 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/service/TemplateService.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.service 2 | 3 | import net.mamoe.mirai.contact.Contact 4 | import net.mamoe.mirai.message.data.buildForwardMessage 5 | import top.colter.mirai.plugin.bilibili.BiliConfig 6 | import top.colter.mirai.plugin.bilibili.BiliData 7 | import top.colter.mirai.plugin.bilibili.api.getDynamicDetail 8 | import top.colter.mirai.plugin.bilibili.api.getLive 9 | import top.colter.mirai.plugin.bilibili.data.DynamicMessage 10 | import top.colter.mirai.plugin.bilibili.data.LIVE_LINK 11 | import top.colter.mirai.plugin.bilibili.data.LiveCloseMessage 12 | import top.colter.mirai.plugin.bilibili.data.LiveMessage 13 | import top.colter.mirai.plugin.bilibili.tasker.DynamicMessageTasker.buildMessage 14 | import top.colter.mirai.plugin.bilibili.tasker.LiveMessageTasker.buildMessage 15 | import top.colter.mirai.plugin.bilibili.tasker.SendTasker.buildMessage 16 | import top.colter.mirai.plugin.bilibili.utils.biliClient 17 | 18 | object TemplateService { 19 | suspend fun listTemplate(type: String, subject: Contact) { 20 | val template = when (type) { 21 | "d" -> BiliConfig.templateConfig.dynamicPush 22 | "l" -> BiliConfig.templateConfig.livePush 23 | "le" -> BiliConfig.templateConfig.liveClose 24 | else -> { 25 | subject.sendMessage("类型错误 d:动态 l:直播 le:直播结束") 26 | return 27 | } 28 | } 29 | 30 | // https://t.bilibili.com/385190177693666264 31 | val dynamic = when (type) { 32 | "d" -> biliClient.getDynamicDetail("385190177693666264")?.buildMessage()!! 33 | "l" -> biliClient.getLive(1, 1)?.rooms?.first()?.buildMessage()!! 34 | "le" -> LiveCloseMessage( 35 | 0,0,"Test", "2022年1月1日 00:00:00", 1640966400, "2022年1月1日 01:02:03", 36 | "1小时 2分钟 3秒", "测试测试测试TEST", "游戏", LIVE_LINK("0") 37 | ) 38 | else -> return 39 | } 40 | subject.sendMessage(buildForwardMessage(subject) { 41 | var pt = 0 42 | subject.bot named dynamic.name at dynamic.timestamp says if (type == "d") "动态推送模板" else "直播推送模板" 43 | subject.bot named dynamic.name at dynamic.timestamp says "下面每个转发消息都代表一个模板推送效果" 44 | for (t in template) { 45 | subject.bot named dynamic.name at dynamic.timestamp + pt says t.key 46 | subject.bot named dynamic.name at dynamic.timestamp + pt says buildForwardMessage(subject) { 47 | when (dynamic) { 48 | is DynamicMessage -> dynamic.buildMessage(t.value, listOf(subject)).forEach { 49 | subject.bot named dynamic.name at dynamic.timestamp + pt says it 50 | } 51 | is LiveMessage -> dynamic.buildMessage(t.value, listOf(subject)).forEach { 52 | subject.bot named dynamic.name at dynamic.timestamp + pt says it 53 | } 54 | is LiveCloseMessage -> dynamic.buildMessage(t.value).forEach { 55 | subject.bot named dynamic.name at dynamic.timestamp + pt says it 56 | } 57 | } 58 | } 59 | pt += 86400 60 | } 61 | //subject.bot named dynamic.uname at dynamic.timestamp + pt says buildString { 62 | // appendLine("请回复模板名: ") 63 | // template.keys.forEach { appendLine(it) } 64 | //} 65 | }) 66 | } 67 | 68 | fun setTemplate(type: String, template: String, subject: String): String { 69 | val pushTemplates = when (type) { 70 | "d" -> BiliConfig.templateConfig.dynamicPush 71 | "l" -> BiliConfig.templateConfig.livePush 72 | "le" -> BiliConfig.templateConfig.liveClose 73 | else -> return "类型错误 d:动态 l:直播 le:直播结束" 74 | } 75 | val push = when (type) { 76 | "d" -> BiliData.dynamicPushTemplate 77 | "l" -> BiliData.livePushTemplate 78 | "le" -> BiliData.liveCloseTemplate 79 | else -> return "类型错误 d:动态 l:直播 le:直播结束" 80 | } 81 | return if (pushTemplates.containsKey(template)) { 82 | push.forEach { (_, u) -> u.remove(subject) } 83 | if (!push.containsKey(template)) push[template] = mutableSetOf() 84 | push[template]!!.add(subject) 85 | "配置完成" 86 | } else "没有这个模板哦 $template" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/BiliCheckTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import top.colter.mirai.plugin.bilibili.BiliConfig 4 | import top.colter.mirai.plugin.bilibili.client.BiliClient 5 | import top.colter.mirai.plugin.bilibili.utils.logger 6 | import java.time.Instant 7 | import java.time.LocalTime 8 | 9 | abstract class BiliCheckTasker( 10 | val taskerName: String? = null 11 | ) : BiliTasker(taskerName) { 12 | 13 | private val intervalTime: Int by lazy { interval } 14 | 15 | protected open var lowSpeedEnable = BiliConfig.enableConfig.lowSpeedEnable 16 | private var lsl = listOf(0, 0) 17 | 18 | protected open var checkReportEnable = true 19 | private val checkReportInterval: Int = BiliConfig.checkConfig.checkReportInterval 20 | private var lastCheck: Long = Instant.now().epochSecond - checkReportInterval * 60 21 | private var checkCount = 0 22 | 23 | companion object { 24 | @JvmStatic 25 | protected val client = BiliClient() 26 | } 27 | 28 | override fun init() { 29 | if (lowSpeedEnable) runCatching { 30 | lsl = BiliConfig.checkConfig.lowSpeed.split("-", "x").map { it.toInt() } 31 | lowSpeedEnable = lsl[0] != lsl[1] 32 | }.onFailure { 33 | logger.error("低频检测参数错误 ${it.message}") 34 | } 35 | } 36 | 37 | override fun before() { 38 | if (checkReportEnable) { 39 | ++ checkCount 40 | val now = Instant.now().epochSecond 41 | if (now - lastCheck >= checkReportInterval * 60){ 42 | logger.debug("$taskerName check running...${checkCount}") 43 | lastCheck = now 44 | checkCount = 0 45 | } 46 | } 47 | } 48 | 49 | override fun after() { 50 | if (lowSpeedEnable) interval = calcTime(intervalTime) 51 | } 52 | 53 | private fun calcTime(time: Int): Int { 54 | return if (lowSpeedEnable) { 55 | val hour = LocalTime.now().hour 56 | return if (lsl[0] > lsl[1]) { 57 | if (lsl[0] <= hour || hour <= lsl[1]) time * lsl[2] else time 58 | } else { 59 | if (lsl[0] <= hour && hour <= lsl[1]) time * lsl[2] else time 60 | } 61 | } else time 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/BiliTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import kotlinx.coroutines.* 4 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 5 | import top.colter.mirai.plugin.bilibili.utils.logger 6 | import kotlin.coroutines.CoroutineContext 7 | 8 | abstract class BiliTasker( 9 | private val taskerName: String? = null 10 | ) : CoroutineScope, CompletableJob by SupervisorJob(BiliBiliDynamic.coroutineContext.job) { 11 | override val coroutineContext: CoroutineContext 12 | get() = this + CoroutineName(taskerName ?: this::class.simpleName ?: "Tasker") 13 | 14 | companion object { 15 | val taskers = mutableListOf() 16 | 17 | fun cancelAll() { 18 | taskers.forEach { 19 | it.cancel() 20 | } 21 | } 22 | } 23 | 24 | private var job: Job? = null 25 | 26 | abstract var interval: Int 27 | open val unitTime: Long = 1000 28 | 29 | protected open fun init() {} 30 | 31 | protected open fun before() {} 32 | protected abstract suspend fun main() 33 | protected open fun after() {} 34 | 35 | override fun start(): Boolean { 36 | job = launch(coroutineContext) { 37 | init() 38 | if (interval == -1) { 39 | before() 40 | main() 41 | after() 42 | } else { 43 | while (isActive) { 44 | try { 45 | before() 46 | main() 47 | after() 48 | } catch (t: Throwable) { 49 | logger.error(this::class.simpleName + t) 50 | delay(120000L) 51 | } 52 | delay(interval * unitTime) 53 | } 54 | } 55 | if (!isActive) logger.error("${this::class.simpleName} 已停止工作!") 56 | } 57 | 58 | return taskers.add(this) 59 | } 60 | 61 | override fun cancel(cause: CancellationException?) { 62 | job?.cancel(cause) 63 | coroutineContext.cancelChildren(cause) 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/CacheClearTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import top.colter.mirai.plugin.bilibili.BiliConfig 4 | import top.colter.mirai.plugin.bilibili.utils.cachePath 5 | import java.nio.file.Path 6 | import kotlin.io.path.forEachDirectoryEntry 7 | import kotlin.io.path.isDirectory 8 | 9 | object CacheClearTasker : BiliTasker() { 10 | override var interval: Int = 60 * 60 * 24 11 | 12 | private val expires by BiliConfig.cacheConfig::expires 13 | 14 | override suspend fun main() { 15 | for (e in expires) { 16 | if (e.value > 0) { 17 | e.key.cachePath().clearExpireFile(e.value) 18 | } 19 | } 20 | } 21 | 22 | private fun Path.clearExpireFile(expire: Int) { 23 | forEachDirectoryEntry { 24 | if (it.isDirectory()) { 25 | it.clearExpireFile(expire) 26 | } else if (System.currentTimeMillis() - it.toFile().lastModified() >= expire * interval * unitTime) { 27 | it.toFile().delete() 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/DynamicCheckTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import kotlinx.coroutines.withTimeout 4 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 5 | import top.colter.mirai.plugin.bilibili.BiliConfig 6 | import top.colter.mirai.plugin.bilibili.BiliData 7 | import top.colter.mirai.plugin.bilibili.api.getNewDynamic 8 | import top.colter.mirai.plugin.bilibili.data.DynamicDetail 9 | import top.colter.mirai.plugin.bilibili.data.DynamicType 10 | import top.colter.mirai.plugin.bilibili.utils.sendAll 11 | import top.colter.mirai.plugin.bilibili.utils.time 12 | import java.time.Instant 13 | 14 | object DynamicCheckTasker : BiliCheckTasker("Dynamic") { 15 | 16 | override var interval = BiliConfig.checkConfig.interval 17 | 18 | private val dynamicChannel by BiliBiliDynamic::dynamicChannel 19 | 20 | private val dynamic by BiliData::dynamic 21 | private val bangumi by BiliData::bangumi 22 | 23 | private val listenAllDynamicMode = false 24 | 25 | private val banType = listOf( 26 | DynamicType.DYNAMIC_TYPE_LIVE, 27 | DynamicType.DYNAMIC_TYPE_LIVE_RCMD, 28 | //DynamicType.DYNAMIC_TYPE_PGC, 29 | //DynamicType.DYNAMIC_TYPE_PGC_UNION 30 | ) 31 | 32 | private const val capacity = 200 33 | private val historyDynamic = ArrayList(capacity) 34 | private var lastIndex = 0 35 | 36 | private var lastDynamic: Long = Instant.now().epochSecond 37 | 38 | override suspend fun main() = withTimeout(180001) { 39 | val dynamicList = client.getNewDynamic() 40 | if (dynamicList != null) { 41 | val followingUsers = dynamic.filter { it.value.contacts.isNotEmpty() }.map { it.key } 42 | val dynamics = dynamicList.items 43 | .filter { 44 | !banType.contains(it.type) 45 | }.filter { 46 | it.time > lastDynamic 47 | }.filter { 48 | !historyDynamic.contains(it.did) 49 | }.filter { 50 | if (listenAllDynamicMode) true 51 | else if (it.type == DynamicType.DYNAMIC_TYPE_PGC || it.type == DynamicType.DYNAMIC_TYPE_PGC_UNION) 52 | bangumi.contains(it.modules.moduleAuthor.mid) 53 | else followingUsers.contains(it.modules.moduleAuthor.mid) 54 | }.sortedBy { 55 | it.time 56 | } 57 | dynamics.map { it.did }.forEach { 58 | historyDynamic.add(lastIndex, it) 59 | lastIndex ++ 60 | if (lastIndex >= capacity) lastIndex = 0 61 | } 62 | //if (dynamics.isNotEmpty()) lastDynamic = dynamics.last().time 63 | dynamicChannel.sendAll(dynamics.map { DynamicDetail(it) }) 64 | } 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/ListenerTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import net.mamoe.mirai.Bot 4 | import net.mamoe.mirai.event.events.BotLeaveEvent 5 | import net.mamoe.mirai.event.events.GroupMessageEvent 6 | import net.mamoe.mirai.event.globalEventChannel 7 | import net.mamoe.mirai.message.data.* 8 | import top.colter.mirai.plugin.bilibili.BiliConfig 9 | import top.colter.mirai.plugin.bilibili.BiliData 10 | import top.colter.mirai.plugin.bilibili.service.DynamicService.removeAllSubscribe 11 | import top.colter.mirai.plugin.bilibili.service.TriggerMode 12 | import top.colter.mirai.plugin.bilibili.service.matchingRegular 13 | import top.colter.mirai.plugin.bilibili.utils.* 14 | 15 | object ListenerTasker : BiliTasker() { 16 | override var interval: Int = -1 17 | 18 | private val triggerMode = BiliConfig.linkResolveConfig.triggerMode 19 | private val returnLink = BiliConfig.linkResolveConfig.returnLink 20 | 21 | override suspend fun main() { 22 | globalEventChannel().subscribeAlways { 23 | val d = group.delegate 24 | if (findContact(d) == null) { 25 | removeAllSubscribe(d) 26 | BiliData.dynamicPushTemplate.forEach { (_, c) -> c.remove(d) } 27 | BiliData.livePushTemplate.forEach { (_, c) -> c.remove(d) } 28 | logger.warning("Bot退出群 ${group.name}(${group.id}) 已删除此群的所有订阅数据") 29 | } 30 | } 31 | 32 | globalEventChannel().subscribeAlways { 33 | var f = false 34 | when (triggerMode) { 35 | TriggerMode.At -> { 36 | val at = message.filterIsInstance(At::class.java) 37 | if (at.isNotEmpty() && at.any { Bot.instances.map { it.id }.contains(it.target) }) { 38 | f = true 39 | } 40 | } 41 | TriggerMode.Always -> f = true 42 | TriggerMode.Never -> f = false 43 | } 44 | if (f) { 45 | val msg = message.filter { it !is At && it !is Image }.toMessageChain().content.trim() 46 | val type = matchingRegular(msg) 47 | if (type != null) { 48 | val ms = subject.sendMessage("加载中...") 49 | val img = type.drawGeneral() 50 | if (img == null) { 51 | ms.recall() 52 | subject.sendMessage("解析失败") 53 | return@subscribeAlways 54 | } 55 | val imgMsg = subject.uploadImage(img, CacheType.DRAW_SEARCH) 56 | if (imgMsg == null) { 57 | ms.recall() 58 | subject.sendMessage("图片上传失败") 59 | return@subscribeAlways 60 | } 61 | subject.sendMessage(buildMessageChain { 62 | + imgMsg 63 | if (returnLink) + PlainText(type.getLink()) 64 | }) 65 | ms.recall() 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/LiveCheckTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import kotlinx.coroutines.withTimeout 4 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 5 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic.liveUsers 6 | import top.colter.mirai.plugin.bilibili.BiliConfig 7 | import top.colter.mirai.plugin.bilibili.BiliData 8 | import top.colter.mirai.plugin.bilibili.api.getLive 9 | import top.colter.mirai.plugin.bilibili.data.LiveDetail 10 | import top.colter.mirai.plugin.bilibili.utils.sendAll 11 | import java.time.Instant 12 | 13 | object LiveCheckTasker : BiliCheckTasker("Live") { 14 | override var interval = BiliConfig.checkConfig.liveInterval 15 | private val liveCloseEnable = BiliConfig.enableConfig.liveCloseNotifyEnable 16 | 17 | private val liveChannel by BiliBiliDynamic::liveChannel 18 | private val dynamic by BiliData::dynamic 19 | 20 | private var lastLive: Long = Instant.now().epochSecond 21 | 22 | override suspend fun main() = withTimeout(180003) { 23 | val liveList = client.getLive() 24 | 25 | if (liveList != null) { 26 | val followingUsers = dynamic.filter { it.value.contacts.isNotEmpty() }.map { it.key } 27 | val lives = liveList.rooms 28 | .filter { 29 | it.liveTime > lastLive 30 | }.filter { 31 | followingUsers.contains(it.uid) 32 | }.sortedBy { 33 | it.liveTime 34 | } 35 | 36 | if (lives.isNotEmpty()) { 37 | lastLive = lives.last().liveTime 38 | liveChannel.sendAll(lives.map { LiveDetail(it) }) 39 | if (liveCloseEnable) liveUsers.putAll(lives.map { it.uid to it.liveTime }) 40 | } 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/LiveCloseCheckTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 4 | import top.colter.mirai.plugin.bilibili.BiliConfig 5 | import top.colter.mirai.plugin.bilibili.api.getLiveStatus 6 | import top.colter.mirai.plugin.bilibili.data.LIVE_LINK 7 | import top.colter.mirai.plugin.bilibili.data.LiveCloseMessage 8 | import top.colter.mirai.plugin.bilibili.utils.formatDuration 9 | import top.colter.mirai.plugin.bilibili.utils.formatTime 10 | import java.time.Instant 11 | 12 | 13 | object LiveCloseCheckTasker : BiliCheckTasker("LiveClose") { 14 | 15 | override var interval: Int = BiliConfig.checkConfig.liveInterval 16 | 17 | override var lowSpeedEnable = false 18 | override var checkReportEnable = false 19 | 20 | private val liveUsers by BiliBiliDynamic::liveUsers 21 | private var nowTime = Instant.now().epochSecond 22 | 23 | override suspend fun main() { 24 | if (liveUsers.isNotEmpty()) { 25 | nowTime = Instant.now().epochSecond 26 | 27 | val liveStatusMap = client.getLiveStatus(liveUsers.map { it.key }) 28 | val liveStatusList = liveStatusMap?.map { it.value }?.filter { it.liveStatus != 1 } 29 | 30 | liveStatusList?.forEach { info -> 31 | val liveTime = liveUsers[info.uid]!! 32 | BiliBiliDynamic.messageChannel.send(LiveCloseMessage( 33 | info.roomId, 34 | info.uid, 35 | info.uname, 36 | liveTime.formatTime, 37 | 0, 38 | nowTime.formatTime, 39 | (nowTime - liveTime).formatDuration(), 40 | info.title, 41 | info.area, 42 | LIVE_LINK(info.roomId.toString()) 43 | )) 44 | liveUsers.remove(info.uid) 45 | } 46 | } 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/tasker/LiveMessageTasker.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.tasker 2 | 3 | import kotlinx.coroutines.withTimeout 4 | import org.jetbrains.skia.Color 5 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 6 | import top.colter.mirai.plugin.bilibili.BiliConfig 7 | import top.colter.mirai.plugin.bilibili.BiliData 8 | import top.colter.mirai.plugin.bilibili.data.LIVE_LINK 9 | import top.colter.mirai.plugin.bilibili.data.LiveInfo 10 | import top.colter.mirai.plugin.bilibili.data.LiveMessage 11 | import top.colter.mirai.plugin.bilibili.draw.makeDrawLive 12 | import top.colter.mirai.plugin.bilibili.draw.makeRGB 13 | import top.colter.mirai.plugin.bilibili.utils.formatTime 14 | import top.colter.mirai.plugin.bilibili.utils.logger 15 | 16 | object LiveMessageTasker : BiliTasker() { 17 | override var interval: Int = 0 18 | 19 | private val liveChannel by BiliBiliDynamic::liveChannel 20 | private val messageChannel by BiliBiliDynamic::messageChannel 21 | 22 | override suspend fun main() { 23 | val liveDetail = liveChannel.receive() 24 | withTimeout(180004) { 25 | val liveInfo = liveDetail.item 26 | logger.debug("直播: ${liveInfo.uname}@${liveInfo.uid}@${liveInfo.title}") 27 | messageChannel.send(liveInfo.buildMessage(liveDetail.contact)) 28 | } 29 | } 30 | 31 | suspend fun LiveInfo.buildMessage(contact: String? = null): LiveMessage { 32 | return LiveMessage( 33 | roomId, 34 | uid, 35 | this.uname, 36 | liveTime.formatTime, 37 | liveTime.toInt(), 38 | title, 39 | cover, 40 | area, 41 | LIVE_LINK(roomId.toString()), 42 | makeLive(), 43 | contact 44 | ) 45 | } 46 | 47 | suspend fun LiveInfo.makeLive(): String? { 48 | return if (BiliConfig.enableConfig.drawEnable) { 49 | val color = BiliData.dynamic[uid]?.color ?: BiliConfig.imageConfig.defaultColor 50 | val colors = color.split(";", ";").map { Color.makeRGB(it.trim()) } 51 | makeDrawLive(colors) 52 | } else null 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/FontUtils.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils 2 | 3 | import org.jetbrains.skia.* 4 | import org.jetbrains.skia.paragraph.FontCollection 5 | import org.jetbrains.skia.paragraph.TypefaceFontProvider 6 | 7 | object FontUtils { 8 | 9 | private val fontMgr = FontMgr.default 10 | private val fontProvider = TypefaceFontProvider() 11 | val fonts = FontCollection().setDynamicFontManager(fontProvider).setDefaultFontManager(fontMgr) 12 | 13 | var defaultFont: Typeface? = null 14 | 15 | private fun registerTypeface(typeface: Typeface?, alias: String? = null) { 16 | fontProvider.registerTypeface(typeface) 17 | if (alias != null) fontProvider.registerTypeface(typeface, alias) 18 | } 19 | 20 | 21 | fun matchFamily(familyName: String): FontStyleSet { 22 | val fa = fontProvider.matchFamily(familyName) 23 | if (fa.count() != 0) { 24 | return fa 25 | } else { 26 | return fontMgr.matchFamily(familyName) 27 | } 28 | } 29 | 30 | fun loadTypeface(path: String, alias: String? = null, index: Int = 0): Typeface { 31 | val face = Typeface.makeFromFile(path, index) 32 | if (defaultFont == null) defaultFont = face 33 | registerTypeface(face, alias) 34 | logger.info("加载字体 ${face.familyName} 成功") 35 | return face 36 | } 37 | 38 | fun loadTypeface(data: Data, index: Int = 0): Typeface { 39 | val face = Typeface.makeFromData(data, index) 40 | registerTypeface(face) 41 | return face 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/Json2DataClass.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.call.* 5 | import io.ktor.client.engine.okhttp.* 6 | import io.ktor.client.request.* 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import kotlinx.serialization.json.* 10 | import java.nio.file.Path 11 | 12 | 13 | val allNullMode = true 14 | val noteValue = false 15 | 16 | suspend fun json2DataClassFile(url: String, baseClassName: String, path: Path) { 17 | val data = json2DataClass(url, baseClassName) 18 | withContext(Dispatchers.IO) { 19 | val file = path.resolve("$baseClassName.kt").toFile() 20 | file.createNewFile() 21 | file.appendText("import kotlinx.serialization.SerialName\n") 22 | file.appendText("import kotlinx.serialization.Serializable\n\n") 23 | file.appendText(data) 24 | } 25 | } 26 | 27 | suspend fun json2DataClass(url: String, baseClassName: String): String { 28 | val client = HttpClient(OkHttp) 29 | val resStr = client.get(url).body() 30 | val resJson = json.parseToJsonElement(resStr) 31 | return resJson.jsonObject.decodeJsonObject(baseClassName) 32 | } 33 | 34 | 35 | private fun JsonObject.decodeJsonObject(objName: String): String { 36 | var obj = "" 37 | val plus = if (allNullMode) "? = null," else "," 38 | return buildString { 39 | appendLine("@Serializable") 40 | appendLine("data class $objName(") 41 | 42 | entries.forEach { 43 | val key = it.key.replace(" ", "_") 44 | if (noteValue && it.value is JsonPrimitive) { 45 | appendLine(" // ${it.value}") 46 | } 47 | appendLine(" @SerialName(\"$key\")") 48 | try { 49 | when (it.value) { 50 | is JsonPrimitive -> { 51 | val attr = it.value.jsonPrimitive.parse() 52 | appendLine(" val ${snakeToCamelLowerFirst(key)}: $attr$plus") 53 | } 54 | is JsonObject -> { 55 | val objKey = snakeToCamel(key) 56 | appendLine(" val ${snakeToCamelLowerFirst(key)}: $objKey$plus") 57 | obj += "\n" + it.value.jsonObject.decodeJsonObject(objKey) 58 | } 59 | is JsonArray -> { 60 | val arr = it.value.jsonArray.first() 61 | val attr = if (arr is JsonPrimitive) { 62 | arr.jsonPrimitive.parse() 63 | } else { 64 | val k = snakeToCamel(key) 65 | obj += "\n" + arr.jsonObject.decodeJsonObject(k) 66 | k 67 | } 68 | appendLine(" val ${snakeToCamelLowerFirst(key)}: List<$attr>$plus") 69 | } 70 | is JsonNull -> { 71 | appendLine(" val ${snakeToCamelLowerFirst(key)}: JsonElement? = null,") 72 | } 73 | } 74 | } catch (e: Exception) { 75 | println(e) 76 | println("Error Key: ${it.key}") 77 | } 78 | } 79 | append(")") 80 | if (obj != "") { 81 | append("{") 82 | appendLine(obj.replace("\n", "\n ")) 83 | append("}") 84 | } 85 | } 86 | } 87 | 88 | private fun JsonPrimitive.parse() = 89 | if (intOrNull != null) "Int" 90 | else if (longOrNull != null) "Long" 91 | else if (booleanOrNull != null) "Boolean" 92 | else if (floatOrNull != null) "Float" 93 | else if (isString) "String" 94 | else "String" 95 | 96 | private fun snakeToCamel(name: String) = 97 | name.split("_").joinToString("") { s -> s.replaceRange(0, 1, s.first().uppercase()) } 98 | 99 | private fun snakeToCamelLowerFirst(name: String): String { 100 | val k = snakeToCamel(name) 101 | return k.replaceRange(0, 1, k.first().lowercase()) 102 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils 2 | 3 | import kotlinx.serialization.SerializationException 4 | import kotlinx.serialization.json.Json 5 | import kotlinx.serialization.json.JsonElement 6 | import kotlinx.serialization.json.decodeFromJsonElement 7 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 8 | import kotlin.io.path.appendText 9 | import kotlin.io.path.createDirectories 10 | import kotlin.io.path.notExists 11 | import kotlin.io.path.writeText 12 | 13 | val json = Json { 14 | prettyPrint = true 15 | ignoreUnknownKeys = true 16 | isLenient = true 17 | allowStructuredMapKeys = true 18 | } 19 | 20 | inline fun String.decode(): T = json.parseToJsonElement(this).decode() 21 | 22 | inline fun JsonElement.decode(): T { 23 | return try { 24 | json.decodeFromJsonElement(this) 25 | }catch (e: SerializationException) { 26 | val time = (System.currentTimeMillis() / 1000).formatTime("yyyy-MM-dd") 27 | 28 | val md5 = e.message?.md5() 29 | val fileName = "$time-$md5.json" 30 | 31 | BiliBiliDynamic.dataFolderPath.resolve("exception").apply { 32 | if (notExists()) createDirectories() 33 | }.resolve(fileName).apply { 34 | if (notExists()) { 35 | writeText(e.stackTraceToString()) 36 | appendText("\n\n\n") 37 | appendText(json.encodeToString(JsonElement.serializer(), this@decode)) 38 | } 39 | } 40 | 41 | BiliBiliDynamic.logger.error("json解析失败,请把 /data/exception/ 目录下的 $fileName 文件反馈给开发者\n${e.message}") 42 | throw e 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/translate/HttpGet.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils.translate 2 | 3 | import java.io.* 4 | import java.net.HttpURLConnection 5 | import java.net.MalformedURLException 6 | import java.net.URL 7 | import java.net.URLEncoder 8 | import java.security.KeyManagementException 9 | import java.security.NoSuchAlgorithmException 10 | import java.security.cert.CertificateException 11 | import java.security.cert.X509Certificate 12 | import javax.net.ssl.HttpsURLConnection 13 | import javax.net.ssl.SSLContext 14 | import javax.net.ssl.TrustManager 15 | import javax.net.ssl.X509TrustManager 16 | 17 | /** 18 | * 百度提供 19 | */ 20 | internal object HttpGet { 21 | internal const val SOCKET_TIMEOUT = 10000 // 10S 22 | internal const val GET = "GET" 23 | operator fun get(host: String, params: Map?): String? { 24 | try { 25 | // 设置SSLContext 26 | val sslcontext = SSLContext.getInstance("TLS") 27 | sslcontext.init(null, arrayOf(myX509TrustManager), null) 28 | val sendUrl = getUrlWithQueryString(host, params) 29 | 30 | // System.out.println("URL:" + sendUrl); 31 | val uri = URL(sendUrl) // 创建URL对象 32 | val conn = uri.openConnection() as HttpURLConnection 33 | if (conn is HttpsURLConnection) { 34 | conn.sslSocketFactory = sslcontext.socketFactory 35 | } 36 | conn.connectTimeout = SOCKET_TIMEOUT // 设置相应超时 37 | conn.requestMethod = GET 38 | val statusCode = conn.responseCode 39 | if (statusCode != HttpURLConnection.HTTP_OK) { 40 | println("Http错误码:$statusCode") 41 | } 42 | 43 | // 读取服务器的数据 44 | val `is` = conn.inputStream 45 | val br = BufferedReader(InputStreamReader(`is`)) 46 | val builder = StringBuilder() 47 | var line: String? = null 48 | while (br.readLine().also { line = it } != null) { 49 | builder.append(line) 50 | } 51 | val text = builder.toString() 52 | close(br) // 关闭数据流 53 | close(`is`) // 关闭数据流 54 | conn.disconnect() // 断开连接 55 | return text 56 | } catch (e: MalformedURLException) { 57 | e.printStackTrace() 58 | } catch (e: IOException) { 59 | e.printStackTrace() 60 | } catch (e: KeyManagementException) { 61 | e.printStackTrace() 62 | } catch (e: NoSuchAlgorithmException) { 63 | e.printStackTrace() 64 | } 65 | return null 66 | } 67 | 68 | fun getUrlWithQueryString(url: String, params: Map?): String { 69 | if (params == null) { 70 | return url 71 | } 72 | val builder = StringBuilder(url) 73 | if (url.contains("?")) { 74 | builder.append("&") 75 | } else { 76 | builder.append("?") 77 | } 78 | var i = 0 79 | for (key in params.keys) { 80 | val value = params[key] 81 | ?: // 过滤空的key 82 | continue 83 | if (i != 0) { 84 | builder.append('&') 85 | } 86 | builder.append(key) 87 | builder.append('=') 88 | builder.append(encode(value)) 89 | i++ 90 | } 91 | return builder.toString() 92 | } 93 | 94 | internal fun close(closeable: Closeable?) { 95 | if (closeable != null) { 96 | try { 97 | closeable.close() 98 | } catch (e: IOException) { 99 | e.printStackTrace() 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * 对输入的字符串进行URL编码, 即转换为%20这种形式 106 | * 107 | * @param input 原文 108 | * @return URL编码. 如果编码失败, 则返回原文 109 | */ 110 | fun encode(input: String?): String { 111 | if (input == null) { 112 | return "" 113 | } 114 | try { 115 | return URLEncoder.encode(input, "utf-8") 116 | } catch (e: UnsupportedEncodingException) { 117 | e.printStackTrace() 118 | } 119 | return input 120 | } 121 | 122 | private val myX509TrustManager: TrustManager = object : X509TrustManager { 123 | override fun getAcceptedIssuers(): Array? { 124 | return null 125 | } 126 | 127 | @Throws(CertificateException::class) 128 | override fun checkServerTrusted(chain: Array, authType: String) { 129 | } 130 | 131 | @Throws(CertificateException::class) 132 | override fun checkClientTrusted(chain: Array, authType: String) { 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/translate/MD5.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils.translate 2 | 3 | import java.io.* 4 | import java.security.MessageDigest 5 | import java.security.NoSuchAlgorithmException 6 | import kotlin.experimental.and 7 | 8 | /** 9 | * MD5编码相关的类 10 | * 11 | * @author wangjingtao 12 | */ 13 | object MD5 { 14 | // 首先初始化一个字符数组,用来存放每个16进制字符 15 | private val hexDigits = charArrayOf( 16 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 17 | 'e', 'f' 18 | ) 19 | 20 | /** 21 | * 获得一个字符串的MD5值 22 | * 23 | * @param input 输入的字符串 24 | * @return 输入字符串的MD5值 25 | */ 26 | fun md5(input: String?): String? { 27 | return if (input == null) null else try { 28 | // 拿到一个MD5转换器(如果想要SHA1参数换成”SHA1”) 29 | val messageDigest = MessageDigest.getInstance("MD5") 30 | // 输入的字符串转换成字节数组 31 | var inputByteArray = ByteArray(0) 32 | try { 33 | inputByteArray = input.toByteArray(charset("utf-8")) 34 | } catch (e: UnsupportedEncodingException) { 35 | e.printStackTrace() 36 | } 37 | // inputByteArray是输入字符串转换得到的字节数组 38 | messageDigest.update(inputByteArray) 39 | // 转换并返回结果,也是字节数组,包含16个元素 40 | val resultByteArray = messageDigest.digest() 41 | // 字符数组转换成字符串返回 42 | byteArrayToHex(resultByteArray) 43 | } catch (e: NoSuchAlgorithmException) { 44 | null 45 | } 46 | } 47 | 48 | /** 49 | * 获取文件的MD5值 50 | * 51 | * @param file 52 | * @return 53 | */ 54 | fun md5(file: File): String? { 55 | try { 56 | if (!file.isFile) { 57 | System.err.println("文件" + file.absolutePath + "不存在或者不是文件") 58 | return null 59 | } 60 | val `in` = FileInputStream(file) 61 | val result = md5(`in`) 62 | `in`.close() 63 | return result 64 | } catch (e: FileNotFoundException) { 65 | e.printStackTrace() 66 | } catch (e: IOException) { 67 | e.printStackTrace() 68 | } 69 | return null 70 | } 71 | 72 | fun md5(`in`: InputStream): String? { 73 | try { 74 | val messagedigest = MessageDigest.getInstance("MD5") 75 | val buffer = ByteArray(1024) 76 | var read = 0 77 | while (`in`.read(buffer).also { read = it } != -1) { 78 | messagedigest.update(buffer, 0, read) 79 | } 80 | `in`.close() 81 | return byteArrayToHex(messagedigest.digest()) 82 | } catch (e: NoSuchAlgorithmException) { 83 | e.printStackTrace() 84 | } catch (e: FileNotFoundException) { 85 | e.printStackTrace() 86 | } catch (e: IOException) { 87 | e.printStackTrace() 88 | } 89 | return null 90 | } 91 | 92 | private fun byteArrayToHex(byteArray: ByteArray): String { 93 | // new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)) 94 | val resultCharArray = CharArray(byteArray.size * 2) 95 | // 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去 96 | var index = 0 97 | for (b in byteArray) { 98 | resultCharArray[index++] = hexDigits[b.toInt().ushr(4) and 0xf] 99 | resultCharArray[index++] = hexDigits[(b and 0xf).toInt()] 100 | } 101 | 102 | // 字符数组组合成字符串返回 103 | return String(resultCharArray) 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/translate/TransApi.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils.translate 2 | 3 | import top.colter.mirai.plugin.bilibili.BiliConfig 4 | import top.colter.mirai.plugin.bilibili.utils.json 5 | import top.colter.mirai.plugin.bilibili.utils.logger 6 | 7 | class TransApi(private val appid: String, private val securityKey: String) { 8 | fun getTransResult(query: String, from: String, to: String): String? { 9 | val params = buildParams(query, from, to) 10 | return HttpGet[TRANS_API_HOST, params] 11 | } 12 | 13 | private fun buildParams(query: String, from: String, to: String): Map { 14 | val params: MutableMap = HashMap() 15 | params["q"] = query 16 | params["from"] = from 17 | params["to"] = to 18 | params["appid"] = appid 19 | 20 | // 随机数 21 | val salt = System.currentTimeMillis().toString() 22 | params["salt"] = salt 23 | 24 | // 签名 25 | val src = appid + query + salt + securityKey // 加密前的原文 26 | params["sign"] = MD5.md5(src) 27 | return params 28 | } 29 | 30 | companion object { 31 | private const val TRANS_API_HOST = "http://api.fanyi.baidu.com/api/trans/vip/translate" 32 | } 33 | } 34 | 35 | var jp = 36 | "[ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ゚゛゜ゝゞゟ゠ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ㍿]".toRegex() 37 | 38 | private val api = TransApi( 39 | BiliConfig.translateConfig.baidu.APP_ID, 40 | BiliConfig.translateConfig.baidu.SECURITY_KEY 41 | ) 42 | 43 | //文本翻译 44 | fun trans(text: String): String? { 45 | if (BiliConfig.enableConfig.translateEnable) { 46 | if (BiliConfig.translateConfig.baidu.SECURITY_KEY != "") { 47 | var msg = text 48 | while (msg.indexOf('[') != -1) { 49 | msg = msg.replaceRange(msg.indexOf('['), msg.indexOf(']') + 1, " ") 50 | } 51 | if (msg.contains(jp) || !msg.contains("[\u4e00-\u9fa5]".toRegex())) { 52 | try { 53 | val resMsg = api.getTransResult(msg, "auto", "zh") 54 | if (resMsg == null) { 55 | logger.error("翻译数据获取失败") 56 | return null 57 | } 58 | val transResult = resMsg.let { json.decodeFromString(TransResult.serializer(), it) } 59 | if (transResult.errorCode != null) { 60 | logger.error("翻译错误 code: ${transResult.errorCode} msg: ${transResult.errorMsg}") 61 | return null 62 | } 63 | if (transResult.from != "zh") { 64 | return buildString { 65 | for (item in transResult.transResult!!) { 66 | appendLine(item.dst) 67 | } 68 | } 69 | } 70 | } catch (e: Exception) { 71 | logger.error("Baidu translation failure! 百度翻译失败! $e") 72 | } 73 | } else return null 74 | } else logger.error("Baidu translation API not configured! 未配置百度翻译API") 75 | } 76 | return null 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/top/colter/mirai/plugin/bilibili/utils/translate/TransResult.kt: -------------------------------------------------------------------------------- 1 | package top.colter.mirai.plugin.bilibili.utils.translate 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TransResult( 8 | @SerialName("from") 9 | val from: String? = null, 10 | @SerialName("to") 11 | val to: String? = null, 12 | @SerialName("trans_result") 13 | val transResult: List? = null, 14 | @SerialName("error_code") 15 | val errorCode: String? = null, 16 | @SerialName("error_msg") 17 | val errorMsg: String? = null, 18 | ) 19 | 20 | @Serializable 21 | data class TransData( 22 | @SerialName("src") 23 | val src: String, 24 | @SerialName("dst") 25 | val dst: String 26 | ) -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | top.colter.mirai.plugin.bilibili.BiliBiliDynamic -------------------------------------------------------------------------------- /src/main/resources/font/FansCard.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/src/main/resources/font/FansCard.ttf -------------------------------------------------------------------------------- /src/main/resources/icon/BILIBILI_LOGO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/icon/DISPUTE.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icon/FORWARD.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/icon/LIVE.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/icon/ORGANIZATION_OFFICIAL_VERIFY.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icon/PERSONAL_OFFICIAL_VERIFY.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icon/RICH_TEXT_NODE_TYPE_BV.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/icon/RICH_TEXT_NODE_TYPE_LOTTERY.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/icon/RICH_TEXT_NODE_TYPE_VOTE.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icon/RICH_TEXT_NODE_TYPE_WEB.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/icon/TOPIC.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/image/Blocked_BG_Day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/src/main/resources/image/Blocked_BG_Day.png -------------------------------------------------------------------------------- /src/main/resources/image/HELP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/src/main/resources/image/HELP.png -------------------------------------------------------------------------------- /src/main/resources/image/IMAGE_MISS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Colter23/bilibili-dynamic-mirai-plugin/0471d29c8244762a0898d0535f8bfdc6d576d7bd/src/main/resources/image/IMAGE_MISS.png -------------------------------------------------------------------------------- /src/test/kotlin/DrawDynamicTest.kt: -------------------------------------------------------------------------------- 1 | package top.colter 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.serialization.decodeFromString 5 | import kotlinx.serialization.json.Json 6 | import net.mamoe.mirai.console.MiraiConsole 7 | import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable 8 | import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.load 9 | import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader 10 | import net.mamoe.mirai.console.util.ConsoleExperimentalApi 11 | import org.jetbrains.skia.Color 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Test 15 | import top.colter.mirai.plugin.bilibili.BiliBiliDynamic 16 | import top.colter.mirai.plugin.bilibili.data.DynamicItem 17 | import top.colter.mirai.plugin.bilibili.draw.drawDynamic 18 | import top.colter.mirai.plugin.bilibili.tasker.DynamicMessageTasker.buildMessage 19 | import top.colter.mirai.plugin.bilibili.tasker.SendTasker.buildMsg 20 | import java.io.File 21 | 22 | class DrawDynamicTest { 23 | private val decoder = Json{ 24 | ignoreUnknownKeys = true 25 | } 26 | private val dids = listOf( 27 | "767166448722247682", 28 | "MASKED_sponsor_only_unlocked" 29 | ) 30 | 31 | @OptIn(ConsoleExperimentalApi::class) 32 | @Before 33 | fun initPlugin() = runBlocking { 34 | MiraiConsoleTerminalLoader.startAsDaemon() 35 | BiliBiliDynamic.load() 36 | BiliBiliDynamic.enable() 37 | } 38 | 39 | private fun loadTestResource(path: String) 40 | = File("src/test/resources/").resolve(path) 41 | 42 | private fun loadDynamicItemString(did: String) 43 | = loadTestResource("json/dynamic_item").resolve("$did.json").readText() 44 | 45 | private suspend fun DynamicItem.drawAndSave(path: String){ 46 | val img = drawDynamic(Color.CYAN, false) 47 | loadTestResource("output/$path.png").writeBytes(img.encodeToData()!!.bytes) 48 | } 49 | 50 | private fun decodeToDynamicItem(did: String): DynamicItem 51 | = decoder.decodeFromString(loadDynamicItemString(did)) 52 | 53 | @Test 54 | fun drawDynamic(): Unit = runBlocking { 55 | dids.forEach { decodeToDynamicItem(it).drawAndSave(it) } 56 | } 57 | 58 | @Test 59 | fun buildDynamicMsg() = runBlocking{ 60 | dids.forEach { did -> 61 | decodeToDynamicItem(did).buildMessage().apply { 62 | println("============== ${this.did}==============") 63 | println("content: $content") 64 | images?.forEach { 65 | println(it) 66 | } 67 | } 68 | println() 69 | } 70 | } 71 | 72 | @Test 73 | fun buildDynamicMsg1() = runBlocking { 74 | decodeToDynamicItem("1031201757795975177").buildMessage().apply { 75 | println("============== ${this.did}==============") 76 | println("content: $content") 77 | images?.forEach { 78 | println(it) 79 | } 80 | } 81 | println() 82 | 83 | } 84 | 85 | @Test 86 | fun buildDynamicMsg2() = runBlocking { 87 | val s = decodeToDynamicItem("1029210402684141574").buildMessage() 88 | 89 | println(buildMsg("{name}@{type}\\n{link}\\n{content}", s, listOf())) 90 | 91 | } 92 | 93 | @OptIn(ConsoleExperimentalApi::class) 94 | @After 95 | fun cleanup() = runBlocking { 96 | MiraiConsole.shutdown() 97 | } 98 | } -------------------------------------------------------------------------------- /src/test/resources/json/dynamic_item/767166448722247682.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": { 3 | "comment_id_str": "226709313", 4 | "comment_type": 11, 5 | "is_only_fans": true, 6 | "jump_url": "//www.bilibili.com/opus/767166448722247682", 7 | "like_icon": { 8 | "action_url": "https://i0.hdslb.com/bfs/garb/item/99d0b1d248c6ff87b18ab9f4fba4d89ecd642374.bin", 9 | "end_url": "", 10 | "id": 40543, 11 | "start_url": "" 12 | }, 13 | "rid_str": "226709313" 14 | }, 15 | "id_str": "767166448722247682", 16 | "modules": { 17 | "module_author": { 18 | "avatar": { 19 | "container_size": { 20 | "height": 1.35, 21 | "width": 1.35 22 | }, 23 | "fallback_layers": { 24 | "is_critical_group": true, 25 | "layers": [ 26 | { 27 | "general_spec": { 28 | "pos_spec": { 29 | "axis_x": 0.675, 30 | "axis_y": 0.675, 31 | "coordinate_pos": 2 32 | }, 33 | "render_spec": { 34 | "opacity": 1 35 | }, 36 | "size_spec": { 37 | "height": 1, 38 | "width": 1 39 | } 40 | }, 41 | "layer_config": { 42 | "is_critical": true, 43 | "tags": { 44 | "AVATAR_LAYER": {}, 45 | "GENERAL_CFG": { 46 | "config_type": 1, 47 | "general_config": { 48 | "web_css_style": { 49 | "borderRadius": "50%" 50 | } 51 | } 52 | } 53 | } 54 | }, 55 | "resource": { 56 | "res_image": { 57 | "image_src": { 58 | "placeholder": 6, 59 | "remote": { 60 | "bfs_style": "widget-layer-avatar", 61 | "url": "https://i0.hdslb.com/bfs/face/84b8a4562e3df5e8f830cad6fcf4b84156659f08.jpg" 62 | }, 63 | "src_type": 1 64 | } 65 | }, 66 | "res_type": 3 67 | }, 68 | "visible": true 69 | }, 70 | { 71 | "general_spec": { 72 | "pos_spec": { 73 | "axis_x": 0.8000000000000002, 74 | "axis_y": 0.8000000000000002, 75 | "coordinate_pos": 1 76 | }, 77 | "render_spec": { 78 | "opacity": 1 79 | }, 80 | "size_spec": { 81 | "height": 0.41666666666666663, 82 | "width": 0.41666666666666663 83 | } 84 | }, 85 | "layer_config": { 86 | "tags": { 87 | "GENERAL_CFG": { 88 | "config_type": 1, 89 | "general_config": { 90 | "web_css_style": { 91 | "background-color": "rgb(255,255,255)", 92 | "border": "2px solid rgba(255,255,255,1)", 93 | "borderRadius": "50%", 94 | "boxSizing": "border-box" 95 | } 96 | } 97 | }, 98 | "ICON_LAYER": {} 99 | } 100 | }, 101 | "resource": { 102 | "res_image": { 103 | "image_src": { 104 | "local": 1, 105 | "src_type": 2 106 | } 107 | }, 108 | "res_type": 3 109 | }, 110 | "visible": true 111 | } 112 | ] 113 | }, 114 | "mid": "107251863" 115 | }, 116 | "face": "https://i0.hdslb.com/bfs/face/84b8a4562e3df5e8f830cad6fcf4b84156659f08.jpg", 117 | "face_nft": false, 118 | "following": null, 119 | "icon_badge": { 120 | "icon": "https://i0.hdslb.com/bfs/garb/item/33e2e72d9a0c855f036b4cb55448f44af67a0635.png", 121 | "render_img": "https://i0.hdslb.com/bfs/activity-plat/static/20230112/3b3c5705bda98d50983f6f47df360fef/IN4E1b8HNg.png", 122 | "text": "专属动态" 123 | }, 124 | "jump_url": "//space.bilibili.com/107251863/dynamic", 125 | "label": "", 126 | "mid": 107251863, 127 | "name": "Nachuan川川", 128 | "official_verify": { 129 | "desc": "", 130 | "type": -1 131 | }, 132 | "pendant": { 133 | "expire": 0, 134 | "image": "", 135 | "image_enhance": "", 136 | "image_enhance_frame": "", 137 | "name": "", 138 | "pid": 0 139 | }, 140 | "pub_action": "", 141 | "pub_location_text": "", 142 | "pub_time": "2023-02-27 08:37", 143 | "pub_ts": 1677458258, 144 | "type": "AUTHOR_TYPE_NORMAL", 145 | "vip": { 146 | "avatar_subscript": 1, 147 | "avatar_subscript_url": "", 148 | "due_date": 1712419200000, 149 | "label": { 150 | "bg_color": "#FB7299", 151 | "bg_style": 1, 152 | "border_color": "", 153 | "img_label_uri_hans": "", 154 | "img_label_uri_hans_static": "https://i0.hdslb.com/bfs/vip/8d4f8bfc713826a5412a0a27eaaac4d6b9ede1d9.png", 155 | "img_label_uri_hant": "", 156 | "img_label_uri_hant_static": "https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png", 157 | "label_theme": "annual_vip", 158 | "path": "", 159 | "text": "年度大会员", 160 | "text_color": "#FFFFFF", 161 | "use_img_label": true 162 | }, 163 | "nickname_color": "#FB7299", 164 | "status": 1, 165 | "theme_type": 0, 166 | "type": 2 167 | } 168 | }, 169 | "module_dynamic": { 170 | "additional": null, 171 | "desc": null, 172 | "major": { 173 | "blocked": { 174 | "bg_img": { 175 | "img_dark": "https://i0.hdslb.com/bfs/activity-plat/static/20221216/c103299ba3500e5000d47f2f0f04712d/wBIsPss7VZ.png", 176 | "img_day": "https://i0.hdslb.com/bfs/activity-plat/static/20221216/c103299ba3500e5000d47f2f0f04712d/eqeFwt8kUe.png" 177 | }, 178 | "blocked_type": 1, 179 | "button": { 180 | "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20230112/3b3c5705bda98d50983f6f47df360fef/qcRJ6sJU91.png", 181 | "jump_url": "https://www.bilibili.com/h5/upower/index?navhide=1\u0026mid=107251863\u0026prePage=onlyFansDynMdlBlocked", 182 | "text": "充电" 183 | }, 184 | "hint_message": "该动态为包月充电专属\n可以给UP主充电后观看", 185 | "icon": { 186 | "img_dark": "https://i0.hdslb.com/bfs/activity-plat/static/20221216/c103299ba3500e5000d47f2f0f04712d/RP513ypCyt.png", 187 | "img_day": "https://i0.hdslb.com/bfs/activity-plat/static/20221216/c103299ba3500e5000d47f2f0f04712d/8gweMAFDvP.png" 188 | } 189 | }, 190 | "type": "MAJOR_TYPE_BLOCKED" 191 | }, 192 | "topic": null 193 | }, 194 | "module_more": { 195 | "three_point_items": [ 196 | { 197 | "label": "举报", 198 | "type": "THREE_POINT_REPORT" 199 | } 200 | ] 201 | }, 202 | "module_stat": { 203 | "comment": { 204 | "count": 0, 205 | "forbidden": false, 206 | "hidden": true 207 | }, 208 | "forward": { 209 | "count": 0, 210 | "disabled": true, 211 | "forbidden": true 212 | }, 213 | "like": { 214 | "count": 0, 215 | "forbidden": false, 216 | "status": false 217 | } 218 | } 219 | }, 220 | "type": "DYNAMIC_TYPE_DRAW", 221 | "visible": true 222 | } 223 | 224 | -------------------------------------------------------------------------------- /src/test/resources/json/dynamic_item/MASKED_sponsor_only_unlocked.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": { 3 | "comment_id_str": "226709313", 4 | "comment_type": 11, 5 | "is_only_fans": true, 6 | "jump_url": "//www.bilibili.com/opus/767166448722247682", 7 | "like_icon": { 8 | "action_url": "https://i0.hdslb.com/bfs/garb/item/99d0b1d248c6ff87b18ab9f4fba4d89ecd642374.bin", 9 | "end_url": "", 10 | "id": 40543, 11 | "start_url": "" 12 | }, 13 | "rid_str": "226709313" 14 | }, 15 | "id_str": "767166448722247682", 16 | "modules": { 17 | "module_author": { 18 | "avatar": { 19 | "container_size": { 20 | "height": 1.35, 21 | "width": 1.35 22 | }, 23 | "fallback_layers": { 24 | "is_critical_group": true, 25 | "layers": [ 26 | { 27 | "general_spec": { 28 | "pos_spec": { 29 | "axis_x": 0.675, 30 | "axis_y": 0.675, 31 | "coordinate_pos": 2 32 | }, 33 | "render_spec": { 34 | "opacity": 1 35 | }, 36 | "size_spec": { 37 | "height": 1, 38 | "width": 1 39 | } 40 | }, 41 | "layer_config": { 42 | "is_critical": true, 43 | "tags": { 44 | "AVATAR_LAYER": {}, 45 | "GENERAL_CFG": { 46 | "config_type": 1, 47 | "general_config": { 48 | "web_css_style": { 49 | "borderRadius": "50%" 50 | } 51 | } 52 | } 53 | } 54 | }, 55 | "resource": { 56 | "res_image": { 57 | "image_src": { 58 | "placeholder": 6, 59 | "remote": { 60 | "bfs_style": "widget-layer-avatar", 61 | "url": "https://i0.hdslb.com/bfs/face/84b8a4562e3df5e8f830cad6fcf4b84156659f08.jpg" 62 | }, 63 | "src_type": 1 64 | } 65 | }, 66 | "res_type": 3 67 | }, 68 | "visible": true 69 | }, 70 | { 71 | "general_spec": { 72 | "pos_spec": { 73 | "axis_x": 0.8000000000000002, 74 | "axis_y": 0.8000000000000002, 75 | "coordinate_pos": 1 76 | }, 77 | "render_spec": { 78 | "opacity": 1 79 | }, 80 | "size_spec": { 81 | "height": 0.41666666666666663, 82 | "width": 0.41666666666666663 83 | } 84 | }, 85 | "layer_config": { 86 | "tags": { 87 | "GENERAL_CFG": { 88 | "config_type": 1, 89 | "general_config": { 90 | "web_css_style": { 91 | "background-color": "rgb(255,255,255)", 92 | "border": "2px solid rgba(255,255,255,1)", 93 | "borderRadius": "50%", 94 | "boxSizing": "border-box" 95 | } 96 | } 97 | }, 98 | "ICON_LAYER": {} 99 | } 100 | }, 101 | "resource": { 102 | "res_image": { 103 | "image_src": { 104 | "local": 1, 105 | "src_type": 2 106 | } 107 | }, 108 | "res_type": 3 109 | }, 110 | "visible": true 111 | } 112 | ] 113 | }, 114 | "mid": "107251863" 115 | }, 116 | "face": "https://i0.hdslb.com/bfs/face/84b8a4562e3df5e8f830cad6fcf4b84156659f08.jpg", 117 | "face_nft": false, 118 | "following": null, 119 | "icon_badge": { 120 | "icon": "https://i0.hdslb.com/bfs/garb/item/33e2e72d9a0c855f036b4cb55448f44af67a0635.png", 121 | "render_img": "https://i0.hdslb.com/bfs/activity-plat/static/20230112/3b3c5705bda98d50983f6f47df360fef/IN4E1b8HNg.png", 122 | "text": "专属动态" 123 | }, 124 | "jump_url": "//space.bilibili.com/107251863/dynamic", 125 | "label": "", 126 | "mid": 107251863, 127 | "name": "Nachuan川川", 128 | "official_verify": { 129 | "desc": "", 130 | "type": -1 131 | }, 132 | "pendant": { 133 | "expire": 0, 134 | "image": "", 135 | "image_enhance": "", 136 | "image_enhance_frame": "", 137 | "name": "", 138 | "pid": 0 139 | }, 140 | "pub_action": "", 141 | "pub_location_text": "", 142 | "pub_time": "2023-02-27 08:37", 143 | "pub_ts": 1677458258, 144 | "type": "AUTHOR_TYPE_NORMAL", 145 | "vip": { 146 | "avatar_subscript": 1, 147 | "avatar_subscript_url": "", 148 | "due_date": 1712419200000, 149 | "label": { 150 | "bg_color": "#FB7299", 151 | "bg_style": 1, 152 | "border_color": "", 153 | "img_label_uri_hans": "", 154 | "img_label_uri_hans_static": "https://i0.hdslb.com/bfs/vip/8d4f8bfc713826a5412a0a27eaaac4d6b9ede1d9.png", 155 | "img_label_uri_hant": "", 156 | "img_label_uri_hant_static": "https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png", 157 | "label_theme": "annual_vip", 158 | "path": "", 159 | "text": "年度大会员", 160 | "text_color": "#FFFFFF", 161 | "use_img_label": true 162 | }, 163 | "nickname_color": "#FB7299", 164 | "status": 1, 165 | "theme_type": 0, 166 | "type": 2 167 | } 168 | }, 169 | "module_dynamic": { 170 | "additional": null, 171 | "desc": { 172 | "rich_text_nodes": [ 173 | { 174 | "orig_text": "分享图片", 175 | "text": "分享图片", 176 | "type": "RICH_TEXT_NODE_TYPE_TEXT" 177 | } 178 | ], 179 | "text": "分享图片" 180 | }, 181 | "major": { 182 | "draw": { 183 | "id": 226709313, 184 | "items": [ 185 | { 186 | "height": 1920, 187 | "size": 579.93, 188 | "src": "https://i0.hdslb.com/bfs/activity-plat/static/20221216/c103299ba3500e5000d47f2f0f04712d/eqeFwt8kUe.png", 189 | "tags": [], 190 | "width": 1080 191 | } 192 | ] 193 | }, 194 | "type": "MAJOR_TYPE_DRAW" 195 | }, 196 | "topic": null 197 | }, 198 | "module_more": { 199 | "three_point_items": [ 200 | { 201 | "label": "举报", 202 | "type": "THREE_POINT_REPORT" 203 | } 204 | ] 205 | }, 206 | "module_stat": { 207 | "comment": { 208 | "count": 0, 209 | "forbidden": false 210 | }, 211 | "forward": { 212 | "count": 0, 213 | "disabled": true, 214 | "forbidden": true 215 | }, 216 | "like": { 217 | "count": 0, 218 | "forbidden": false, 219 | "status": false 220 | } 221 | } 222 | }, 223 | "type": "DYNAMIC_TYPE_DRAW", 224 | "visible": true 225 | } --------------------------------------------------------------------------------