mirror of
https://github.com/LamGC/ScalaBot.git
synced 2025-04-30 06:37:29 +00:00
Compare commits
245 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d160fa7ee5 | ||
|
b053388f82 | ||
818cb21df7 | |||
c681ebc4c1 | |||
44b7b5122f | |||
bf4adc8437 | |||
|
47d34b0fd8 | ||
|
14098d285c | ||
|
ad359935d6 | ||
3e1bf4991d | |||
bd7f556c31 | |||
|
9c069adc4b | ||
|
96a06c48ee | ||
ca4bfee03e | |||
16dc98fa85 | |||
8299316af8 | |||
e1c87aeae4 | |||
|
232e318522 | ||
bac7239513 | |||
cc7ef37539 | |||
4cf670a4d0 | |||
|
d6781dbc71 | ||
63f68acccd | |||
506f073bf6 | |||
9dc4bfa28f | |||
92a1deac50 | |||
0e2ce68a9c | |||
d57e63f8de | |||
|
73653cabc6 | ||
|
9e87591671 | ||
|
126092487d | ||
|
44bc52050c | ||
|
f809076df0 | ||
|
b34f05a90b | ||
|
ddc4de1340 | ||
|
a9134f378e | ||
|
dbb2fcdee1 | ||
|
ce00369a22 | ||
|
65bac28303 | ||
|
8bd6d5ffe5 | ||
|
1c4cc122c9 | ||
|
6083f4fc68 | ||
|
b33de4d63a | ||
fd67eb99eb | |||
ba306e679f | |||
|
f188d5bfa9 | ||
|
376086daa1 | ||
|
cee8797c40 | ||
|
77b5867dd7 | ||
|
33eefd9e60 | ||
|
0c81ca6d5f | ||
|
705150b961 | ||
|
e6f0550985 | ||
|
90c57ff9b1 | ||
|
d773cba776 | ||
|
f1c36aacdb | ||
|
fe9e7059c8 | ||
|
b42cc6bd2c | ||
|
ebfc387505 | ||
|
340f6ba3f3 | ||
|
e8f9dbbb05 | ||
|
cdbfa822b0 | ||
|
6d5ce324a5 | ||
|
734a871fdc | ||
|
a9ee432dec | ||
|
1fc386a226 | ||
|
40524e89c6 | ||
|
c972759ffb | ||
|
9b169ac957 | ||
f93a41af96 | |||
|
8eb993da3e | ||
|
9065df0e04 | ||
|
9514c7b57e | ||
24219df63b | |||
|
14e9c4d686 | ||
|
8e83f714a7 | ||
|
939aed5850 | ||
|
a839cc63e6 | ||
|
8be3f770bf | ||
b1e7643b09 | |||
4537e08107 | |||
3ea1eec2dd | |||
|
fe9978dbe8 | ||
|
aece8221f1 | ||
|
4c9256f82f | ||
|
3f2e4e2d1f | ||
|
3810224667 | ||
|
3241bd2da9 | ||
|
dcac53fe65 | ||
|
0bc77ada5f | ||
|
f0b45568d1 | ||
|
a3dd9d4114 | ||
|
3afe3b52ed | ||
|
bdf75f1046 | ||
|
e834d4c7c7 | ||
|
2e1fae87b3 | ||
|
191bef5d68 | ||
|
55c435c4c0 | ||
|
758e0a9f16 | ||
|
5814817f3a | ||
|
2641901240 | ||
|
0070b400bb | ||
|
73e20ab737 | ||
|
03d24fbfe3 | ||
|
53e47353e8 | ||
|
81a97ee8e3 | ||
|
dbcbc88e1b | ||
|
f233a25323 | ||
|
2c91884db8 | ||
6b92b7e377 | |||
375b815659 | |||
f26d642320 | |||
2db0b78962 | |||
8b76a9aa9b | |||
bbc3288535 | |||
|
45ad734c7b | ||
|
1eaed8ce6f | ||
|
a624fcd37d | ||
|
59aa83c93e | ||
3ea0f4eacb | |||
d3e18d80ca | |||
1e0eebbd7e | |||
|
6f3c1ec592 | ||
|
c013dafdd7 | ||
|
e0bfd8796c | ||
|
67d561c0ab | ||
63687da0bb | |||
|
cd76249c33 | ||
|
5a8afd1549 | ||
651569dc8f | |||
bf2ea9a367 | |||
540fe84f26 | |||
712378b3ff | |||
961382fb56 | |||
34d9ece6d7 | |||
30a2cb34d5 | |||
c94e0476b5 | |||
6e5cd07c51 | |||
d4b1438b0b | |||
90110335f5 | |||
9c32d26c0d | |||
dfab6b14bd | |||
437cee499a | |||
4c30a1ac68 | |||
3932db11a1 | |||
d18c059498 | |||
e6b581b8cd | |||
26d7443c87 | |||
2bf4eb684e | |||
5251b62733 | |||
3ec5a8e9c3 | |||
62f6c08cd2 | |||
5e6f64f056 | |||
a036695330 | |||
e9c975f5c5 | |||
9aac42d414 | |||
5add6d9909 | |||
eee6b7d2c9 | |||
ff396425a7 | |||
580d9122e5 | |||
2a607f1129 | |||
2d6da7c1ae | |||
6235c5f51a | |||
255a02c93c | |||
dce28be9c7 | |||
673c6d8392 | |||
d586ca378e | |||
3ba4364a07 | |||
eda0e522cd | |||
c7fedf3882 | |||
a7de85eacb | |||
b6013e2fbe | |||
f79a4e4ff3 | |||
93b9c6b727 | |||
a8a0a9576f | |||
e8711e9974 | |||
93685e9440 | |||
92b7e84b3a | |||
8c4e48e3eb | |||
7f7b2b8895 | |||
441991b705 | |||
51d036c4c6 | |||
3c54c33364 | |||
43dd0e7bea | |||
c144755913 | |||
9ed55204c0 | |||
9b7fc30512 | |||
27dc26160d | |||
ae411ce829 | |||
1afe0f07a8 | |||
cf8e746bd4 | |||
fc66cd16f4 | |||
1340f0aa32 | |||
099c452fe7 | |||
aa31bcd3a8 | |||
5843e37196 | |||
06acc78180 | |||
db010d6c86 | |||
61b611b22e | |||
c7c24fa454 | |||
045b3e5d54 | |||
d6b25c4560 | |||
581eeba20b | |||
896305f4a3 | |||
df484d6bd7 | |||
b8a99a4491 | |||
128e33e545 | |||
85e59f4a64 | |||
8f8d763566 | |||
084280564a | |||
4a160ad42b | |||
a1790a0716 | |||
b12758bd18 | |||
a2667438f2 | |||
d5e66156b9 | |||
8a33448b19 | |||
48a5c27cf7 | |||
0c252f69fb | |||
a55f00edf0 | |||
45244c1fb1 | |||
cfdfa21619 | |||
8e0bf3c22b | |||
c64f5e739b | |||
289b9678f2 | |||
dbc4232dd6 | |||
c662b970f0 | |||
f148c21390 | |||
c41aac735c | |||
ae64de00e7 | |||
215a4670db | |||
c5fe96c02d | |||
508f14f271 | |||
35c77f6093 | |||
7e48f4bf0b | |||
9c05726849 | |||
ac0a398afc | |||
145e5a2141 | |||
b5c85e213b | |||
746221a085 | |||
24f34aa27f | |||
31366575a9 | |||
37c3275bb6 | |||
72e26bd677 | |||
9aab3c2a24 | |||
cac055bb08 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
**/
|
||||||
|
!scalabot-app/build/install/
|
33
.github/dependabot.yml
vendored
Normal file
33
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "gradle"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
# - package-ecosystem: "gradle"
|
||||||
|
# directory: "/scalabot-app"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
|
# - package-ecosystem: "gradle"
|
||||||
|
# directory: "/scalabot-meta"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
|
# - package-ecosystem: "gradle"
|
||||||
|
# directory: "/scalabot-extension"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
|
# - package-ecosystem: "gradle"
|
||||||
|
# directory: "/scalabot-ext-example"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
33
.github/workflows/binary-compatibility-verification.yml
vendored
Normal file
33
.github/workflows/binary-compatibility-verification.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Binary compatibility verification (for API)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'scalabot-meta/**'
|
||||||
|
- 'scalabot-extension/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'scalabot-meta/**'
|
||||||
|
- 'scalabot-extension/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
apiCompatibilityCheck:
|
||||||
|
timeout-minutes: 8
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt-hotspot'
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build and run binary compatibility verification
|
||||||
|
run: ./gradlew apiCheck
|
32
.github/workflows/build-and-test.yml
vendored
Normal file
32
.github/workflows/build-and-test.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||||
|
|
||||||
|
name: Build and test project
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
timeout-minutes: 8
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt-hotspot'
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build and test
|
||||||
|
run: ./gradlew clean test
|
62
.github/workflows/create-release.yml
vendored
Normal file
62
.github/workflows/create-release.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
name: Create release draft
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
|
||||||
|
# 该 Action 有以下步骤:
|
||||||
|
# 1. 拉取并构建代码, 然后生成 Application 发行包;
|
||||||
|
# 2. 创建 Release, 并标记为 Draft(草稿);
|
||||||
|
# 3. 上传 Application 发行包;
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# 创建更新日志.
|
||||||
|
- name: 'Get Previous tag'
|
||||||
|
id: previous-tag
|
||||||
|
uses: younited/get-previous-tag-action@v1.1.0
|
||||||
|
with:
|
||||||
|
match: "v*.*.*"
|
||||||
|
- name: Set up Python 3
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Install Commitizen
|
||||||
|
run: pip install -U commitizen
|
||||||
|
- name: Create Change log
|
||||||
|
run: cz ch --start-rev ${{ steps.previous-tag.outputs.previous-tag }} --file-name ${{ github.workspace }}/CURRENT_CHANGELOG.md
|
||||||
|
|
||||||
|
# 开始构建项目.
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt-hotspot'
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build and test
|
||||||
|
run: ./gradlew clean test assembleDist
|
||||||
|
|
||||||
|
# 创建新的发行版本
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
body_path: ${{ github.workspace }}/CURRENT_CHANGELOG.md
|
||||||
|
files: |
|
||||||
|
*/build/distributions/*
|
||||||
|
*/build/libs/*
|
46
.github/workflows/publish-artifacts.yml
vendored
Normal file
46
.github/workflows/publish-artifacts.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: Publish artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: lamgc/scalabot
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-container-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt-hotspot'
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build and test
|
||||||
|
run: ./gradlew clean test installDist
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push container image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.IMAGE_NAME }}:latest, ${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
46
.github/workflows/release-dev-container-image.yml
vendored
Normal file
46
.github/workflows/release-dev-container-image.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: Build development version container image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
timeout-minutes: 15
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt-hotspot'
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build project and install Distribution package
|
||||||
|
run: ./gradlew clean test installDist
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push container image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: lamgc/scalabot:dev
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM eclipse-temurin:21-jdk-alpine
|
||||||
|
|
||||||
|
ENV BOT_DATA_PATH /scalabot/data/
|
||||||
|
WORKDIR /scalabot/run/
|
||||||
|
|
||||||
|
CMD ["/scalabot/app/bin/scalabot-app"]
|
||||||
|
|
||||||
|
COPY scalabot-app/build/install/scalabot-app/ /scalabot/app/
|
@ -1,7 +1,11 @@
|
|||||||
|
<div style="text-align: center;">
|
||||||
|
|
||||||
# ScalaBot
|
# ScalaBot
|
||||||
|
|
||||||
基于 [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots) 的可扩展机器人服务器。 Extensible robot server based
|
基于 [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots) 的可扩展机器人服务器。
|
||||||
on [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots).
|
Extensible robot server based on [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots).
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## 背景
|
## 背景
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "2.1.0" apply false
|
||||||
|
id("org.jetbrains.kotlinx.kover") version "0.8.3" apply false
|
||||||
|
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.16.3" apply false
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@ -7,5 +13,5 @@ allprojects {
|
|||||||
|
|
||||||
}
|
}
|
||||||
group = "net.lamgc"
|
group = "net.lamgc"
|
||||||
version = "0.3.0"
|
version = "0.8.0-1"
|
||||||
}
|
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
10
gradlew
vendored
Normal file → Executable file
10
gradlew
vendored
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright ? 2015-2021 the original authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -32,10 +32,10 @@
|
|||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
# * functions;
|
# * functions;
|
||||||
# * expansions ?$var?, ?${var}?, ?${var:-default}?, ?${var+SET}?,
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
# ?${var#prefix}?, ?${var%suffix}?, and ?$( cmd )?;
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
# * compound commands having a testable exit status, especially ?case?;
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
# * various built-in commands including ?command?, ?set?, and ?ulimit?.
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
#
|
#
|
||||||
# Important for patching:
|
# Important for patching:
|
||||||
#
|
#
|
||||||
|
0
gradlew.bat
vendored
Normal file → Executable file
0
gradlew.bat
vendored
Normal file → Executable file
@ -1,17 +1,19 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.6.10"
|
kotlin("jvm")
|
||||||
application
|
application
|
||||||
// id("org.jetbrains.kotlin") version "1.6.10"
|
id("org.jetbrains.kotlinx.kover")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(":scalabot-meta"))
|
||||||
implementation(project(":scalabot-extension"))
|
implementation(project(":scalabot-extension"))
|
||||||
|
|
||||||
implementation("org.slf4j:slf4j-api:1.7.36")
|
implementation("org.slf4j:slf4j-api:2.0.11")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
implementation("io.github.microutils:kotlin-logging:3.0.5")
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
implementation("ch.qos.logback:logback-classic:1.5.12")
|
||||||
|
|
||||||
val aetherVersion = "1.1.0"
|
val aetherVersion = "1.1.0"
|
||||||
implementation("org.eclipse.aether:aether-api:$aetherVersion")
|
implementation("org.eclipse.aether:aether-api:$aetherVersion")
|
||||||
@ -21,30 +23,35 @@ dependencies {
|
|||||||
implementation("org.eclipse.aether:aether-transport-http:$aetherVersion")
|
implementation("org.eclipse.aether:aether-transport-http:$aetherVersion")
|
||||||
implementation("org.eclipse.aether:aether-connector-basic:$aetherVersion")
|
implementation("org.eclipse.aether:aether-connector-basic:$aetherVersion")
|
||||||
implementation("org.apache.maven:maven-aether-provider:3.3.9")
|
implementation("org.apache.maven:maven-aether-provider:3.3.9")
|
||||||
|
implementation("org.codehaus.plexus:plexus-utils:3.5.1")
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.20")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:2.1.0")
|
||||||
implementation("com.google.code.gson:gson:2.9.0")
|
implementation("com.google.code.gson:gson:2.11.0")
|
||||||
|
|
||||||
implementation("org.jdom:jdom2:2.0.6.1")
|
implementation("org.jdom:jdom2:2.0.6.1")
|
||||||
|
|
||||||
implementation("org.telegram:telegrambots-abilities:6.0.1")
|
implementation("org.telegram:telegrambots-abilities:8.0.0")
|
||||||
implementation("org.telegram:telegrambots:6.0.1")
|
implementation("org.telegram:telegrambots-longpolling:8.0.0")
|
||||||
|
implementation("org.telegram:telegrambots-client:8.0.0")
|
||||||
|
|
||||||
implementation("io.prometheus:simpleclient:0.15.0")
|
implementation("io.prometheus:simpleclient:0.16.0")
|
||||||
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
|
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation("io.mockk:mockk:1.12.3")
|
testImplementation("io.mockk:mockk:1.13.13")
|
||||||
|
testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
jvmArgs("--add-opens", "java.base/java.util=ALL-UNNAMED", "--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions.jvmTarget = "11"
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@ -54,3 +61,8 @@ application {
|
|||||||
tasks.jar.configure {
|
tasks.jar.configure {
|
||||||
exclude("**/logback-test.xml")
|
exclude("**/logback-test.xml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||||
|
isPreserveFileTimestamps = false
|
||||||
|
isReproducibleFileOrder = true
|
||||||
|
}
|
||||||
|
@ -3,175 +3,104 @@ package net.lamgc.scalabot
|
|||||||
import ch.qos.logback.core.PropertyDefinerBase
|
import ch.qos.logback.core.PropertyDefinerBase
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.JsonArray
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import net.lamgc.scalabot.util.ArtifactSerializer
|
import net.lamgc.scalabot.config.*
|
||||||
import net.lamgc.scalabot.util.AuthenticationSerializer
|
import net.lamgc.scalabot.config.serializer.*
|
||||||
import net.lamgc.scalabot.util.MavenRepositoryConfigSerializer
|
|
||||||
import net.lamgc.scalabot.util.ProxyTypeSerializer
|
|
||||||
import org.eclipse.aether.artifact.Artifact
|
import org.eclipse.aether.artifact.Artifact
|
||||||
import org.eclipse.aether.repository.Authentication
|
import org.eclipse.aether.repository.Authentication
|
||||||
import org.eclipse.aether.repository.Proxy
|
import org.eclipse.aether.repository.Proxy
|
||||||
import org.eclipse.aether.repository.RemoteRepository
|
import org.eclipse.aether.repository.RemoteRepository
|
||||||
import org.eclipse.aether.repository.RepositoryPolicy
|
import org.eclipse.aether.repository.RepositoryPolicy
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
import org.slf4j.event.Level
|
||||||
import org.telegram.telegrambots.meta.ApiConstants
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.system.exitProcess
|
import java.util.function.Supplier
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
private val log = KotlinLogging.logger { }
|
private val log = KotlinLogging.logger { }
|
||||||
|
|
||||||
/**
|
internal fun ProxyType.toJavaProxyType(): java.net.Proxy.Type? {
|
||||||
* 机器人帐号信息.
|
return when (this) {
|
||||||
* @property name 机器人名称, 建议与实际设定的名称相同.
|
ProxyType.NO_PROXY -> null
|
||||||
* @property token 机器人 API Token.
|
ProxyType.HTTP -> java.net.Proxy.Type.HTTP
|
||||||
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
|
ProxyType.HTTPS -> java.net.Proxy.Type.HTTP
|
||||||
*/
|
ProxyType.SOCKS4 -> java.net.Proxy.Type.SOCKS
|
||||||
internal data class BotAccount(
|
ProxyType.SOCKS5 -> java.net.Proxy.Type.SOCKS
|
||||||
val name: String,
|
|
||||||
val token: String,
|
|
||||||
val creatorId: Long = -1
|
|
||||||
) {
|
|
||||||
|
|
||||||
val id
|
|
||||||
// 不要想着每次获取都要从 token 里取出有性能损耗.
|
|
||||||
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
|
|
||||||
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
|
|
||||||
get() = token.substringBefore(":").toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 机器人配置.
|
|
||||||
* @property account 机器人帐号信息, 用于访问 API.
|
|
||||||
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
|
|
||||||
* @property extensions 该机器人启用的扩展.
|
|
||||||
* @property proxy 为该机器人单独设置的代理配置, 如无设置, 则使用 AppConfig 中的代理配置.
|
|
||||||
*/
|
|
||||||
internal data class BotConfig(
|
|
||||||
val enabled: Boolean = true,
|
|
||||||
val account: BotAccount,
|
|
||||||
val disableBuiltInAbility: Boolean = false,
|
|
||||||
val autoUpdateCommandList: Boolean = false,
|
|
||||||
/*
|
|
||||||
* 使用构件坐标来选择机器人所使用的扩展包.
|
|
||||||
* 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id,
|
|
||||||
* 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目时一定会设置的,
|
|
||||||
* 所以就直接用了. :P
|
|
||||||
*/
|
|
||||||
val extensions: Set<Artifact>,
|
|
||||||
val proxy: ProxyConfig? = ProxyConfig(),
|
|
||||||
val baseApiUrl: String? = ApiConstants.BASE_URL
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 代理配置.
|
|
||||||
* @property type 代理类型.
|
|
||||||
* @property host 代理服务端地址.
|
|
||||||
* @property port 代理服务端端口.
|
|
||||||
*/
|
|
||||||
internal data class ProxyConfig(
|
|
||||||
val type: DefaultBotOptions.ProxyType = DefaultBotOptions.ProxyType.NO_PROXY,
|
|
||||||
val host: String = "127.0.0.1",
|
|
||||||
val port: Int = 1080
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun toAetherProxy(): Proxy? {
|
|
||||||
return if (type == DefaultBotOptions.ProxyType.HTTP) {
|
|
||||||
Proxy(Proxy.TYPE_HTTP, host, port)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class MetricsConfig(
|
internal fun ProxyConfig.toAetherProxy(): Proxy? {
|
||||||
val enable: Boolean = false,
|
val typeStr = when (type) {
|
||||||
val port: Int = 9386,
|
ProxyType.HTTP -> Proxy.TYPE_HTTP
|
||||||
val bindAddress: String? = "0.0.0.0"
|
ProxyType.HTTPS -> Proxy.TYPE_HTTPS
|
||||||
)
|
else -> return null
|
||||||
|
}
|
||||||
|
return Proxy(typeStr, host, port)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
internal fun MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig? = null): RemoteRepository {
|
||||||
* Maven 远端仓库配置.
|
val repositoryId = if (id == null) {
|
||||||
* @property url 仓库地址.
|
val generatedRepoId = createDefaultRepositoryId()
|
||||||
* @property proxy 访问仓库所使用的代理, 仅支持 http/https 代理.
|
log.debug { "仓库 Url `$url` 未设置仓库 Id, 已分配缺省 Id: $generatedRepoId" }
|
||||||
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
|
generatedRepoId
|
||||||
*/
|
} else if ("local".contentEquals(id, ignoreCase = true)) {
|
||||||
internal data class MavenRepositoryConfig(
|
val generatedRepoId = createDefaultRepositoryId()
|
||||||
val id: String? = null,
|
log.debug { "仓库 Url `$url` 不允许使用 `local` 作为仓库 Id, 已分配缺省 Id: $generatedRepoId" }
|
||||||
val url: URL,
|
generatedRepoId
|
||||||
val proxy: Proxy? = Proxy("http", "127.0.0.1", 1080),
|
} else {
|
||||||
val layout: String = "default",
|
id
|
||||||
val enableReleases: Boolean = true,
|
}
|
||||||
val enableSnapshots: Boolean = true,
|
val builder = RemoteRepository.Builder(repositoryId, checkRepositoryLayout(layout), url.toString())
|
||||||
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
|
if (proxy != null) {
|
||||||
val authentication: Authentication? = null
|
val selfProxy = proxy!!
|
||||||
) {
|
builder.setProxy(selfProxy)
|
||||||
|
log.debug { "仓库 $repositoryId 已使用独立的代理配置: ${selfProxy.type}://${selfProxy.host}:${selfProxy.port}" }
|
||||||
fun toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
|
} else if (proxyConfig != null) {
|
||||||
val builder =
|
if (proxyConfig.type in (ProxyType.HTTP..ProxyType.HTTPS)) {
|
||||||
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
|
|
||||||
if (proxy != null) {
|
|
||||||
builder.setProxy(proxy)
|
|
||||||
} else if (proxyConfig.type == DefaultBotOptions.ProxyType.HTTP) {
|
|
||||||
builder.setProxy(proxyConfig.toAetherProxy())
|
builder.setProxy(proxyConfig.toAetherProxy())
|
||||||
|
log.debug { "仓库 $repositoryId 已使用 全局/Bot 代理配置: $proxyConfig" }
|
||||||
|
} else {
|
||||||
|
log.debug { "仓库 $repositoryId 不支持 全局/Bot 的代理配置: `$proxyConfig` (仅支持 HTTP 和 HTTPS)" }
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
builder.setReleasePolicy(
|
log.debug { "仓库 $repositoryId 不使用代理." }
|
||||||
RepositoryPolicy(
|
|
||||||
enableReleases,
|
|
||||||
RepositoryPolicy.UPDATE_POLICY_NEVER,
|
|
||||||
RepositoryPolicy.CHECKSUM_POLICY_FAIL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
builder.setSnapshotPolicy(
|
|
||||||
RepositoryPolicy(
|
|
||||||
enableSnapshots,
|
|
||||||
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
|
|
||||||
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return builder.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
builder.setReleasePolicy(
|
||||||
fun checkRepositoryLayout(layoutType: String): String {
|
RepositoryPolicy(
|
||||||
val type = layoutType.trim().lowercase()
|
enableReleases,
|
||||||
if (type != "default" && type != "legacy") {
|
RepositoryPolicy.UPDATE_POLICY_NEVER,
|
||||||
throw IllegalArgumentException("Invalid layout type (expecting 'default' or 'legacy')")
|
RepositoryPolicy.CHECKSUM_POLICY_FAIL
|
||||||
}
|
)
|
||||||
return type
|
)
|
||||||
}
|
builder.setSnapshotPolicy(
|
||||||
|
RepositoryPolicy(
|
||||||
|
enableSnapshots,
|
||||||
|
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
|
||||||
|
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
private val repoNumber = AtomicInteger(1)
|
return builder.build()
|
||||||
|
|
||||||
fun createDefaultRepositoryId(): String {
|
|
||||||
return "Repository-${repoNumber.getAndIncrement()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun checkRepositoryLayout(layoutType: String): String {
|
||||||
* ScalaBot App 配置.
|
val type = layoutType.trim().lowercase()
|
||||||
*
|
if (type != "default" && type != "legacy") {
|
||||||
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
|
throw IllegalArgumentException("Invalid layout type (expecting 'default' or 'legacy')")
|
||||||
* @property proxy Telegram API 代理配置.
|
}
|
||||||
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
|
return type
|
||||||
* @property mavenRepositories Maven 远端仓库配置.
|
}
|
||||||
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
|
|
||||||
*/
|
private val repoNumberGenerator = AtomicInteger(1)
|
||||||
internal data class AppConfig(
|
|
||||||
val proxy: ProxyConfig = ProxyConfig(),
|
private fun createDefaultRepositoryId(): String {
|
||||||
val metrics: MetricsConfig = MetricsConfig(),
|
return "Repository-${repoNumberGenerator.getAndIncrement()}"
|
||||||
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
|
}
|
||||||
val mavenLocalRepository: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 需要用到的路径.
|
* 需要用到的路径.
|
||||||
@ -179,9 +108,9 @@ internal data class AppConfig(
|
|||||||
* 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径.
|
* 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径.
|
||||||
*/
|
*/
|
||||||
internal enum class AppPaths(
|
internal enum class AppPaths(
|
||||||
private val pathSupplier: () -> String = { fileSupplier.invoke().canonicalPath },
|
private val pathSupplier: PathSupplier,
|
||||||
private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer,
|
private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer,
|
||||||
private val fileSupplier: () -> File = { File(pathSupplier()) }
|
private val fileSupplier: FileSupplier,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 数据根目录.
|
* 数据根目录.
|
||||||
@ -190,7 +119,7 @@ internal enum class AppPaths(
|
|||||||
*
|
*
|
||||||
* 提示: 结尾不带 `/`.
|
* 提示: 结尾不带 `/`.
|
||||||
*/
|
*/
|
||||||
DATA_ROOT(fileSupplier = {
|
DATA_ROOT(fileSupplier = FileSupplier {
|
||||||
File(
|
File(
|
||||||
System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH)
|
System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH)
|
||||||
?: System.getProperty("user.dir") ?: "."
|
?: System.getProperty("user.dir") ?: "."
|
||||||
@ -202,10 +131,10 @@ internal enum class AppPaths(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DEFAULT_CONFIG_APPLICATION({ "$DATA_ROOT/config.json" }, {
|
CONFIG_APPLICATION(PathSupplier { "$DATA_ROOT/config.json" }, {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
file.bufferedWriter(StandardCharsets.UTF_8).use {
|
file.bufferedWriter(StandardCharsets.UTF_8).use {
|
||||||
GsonConst.botConfigGson.toJson(
|
GsonConst.appConfigGson.toJson(
|
||||||
AppConfig(
|
AppConfig(
|
||||||
mavenRepositories = listOf(
|
mavenRepositories = listOf(
|
||||||
MavenRepositoryConfig(
|
MavenRepositoryConfig(
|
||||||
@ -218,13 +147,13 @@ internal enum class AppPaths(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
DEFAULT_CONFIG_BOT({ "$DATA_ROOT/bot.json" }, {
|
CONFIG_BOT(PathSupplier { "$DATA_ROOT/bot.json" }, {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
file.bufferedWriter(StandardCharsets.UTF_8).use {
|
file.bufferedWriter(StandardCharsets.UTF_8).use {
|
||||||
GsonConst.botConfigGson.toJson(
|
GsonConst.botConfigGson.toJson(
|
||||||
setOf(
|
setOf(
|
||||||
BotConfig(
|
BotConfig(
|
||||||
enabled = false,
|
enabled = true,
|
||||||
proxy = ProxyConfig(),
|
proxy = ProxyConfig(),
|
||||||
account = BotAccount(
|
account = BotAccount(
|
||||||
"Bot Username",
|
"Bot Username",
|
||||||
@ -244,10 +173,25 @@ internal enum class AppPaths(
|
|||||||
TEMP({ "$DATA_ROOT/tmp/" })
|
TEMP({ "$DATA_ROOT/tmp/" })
|
||||||
;
|
;
|
||||||
|
|
||||||
val file: File
|
constructor(pathSupplier: PathSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
|
||||||
get() = fileSupplier.invoke()
|
fileSupplier = FileSupplier { File(pathSupplier.path).canonicalFile },
|
||||||
val path: String
|
pathSupplier = pathSupplier,
|
||||||
get() = pathSupplier.invoke()
|
initializer = initializer
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(fileSupplier: FileSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
|
||||||
|
fileSupplier = fileSupplier,
|
||||||
|
pathSupplier = PathSupplier { fileSupplier.file.canonicalPath },
|
||||||
|
initializer = initializer
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(pathSupplier: () -> String) : this(
|
||||||
|
fileSupplier = FileSupplier { File(pathSupplier.invoke()).canonicalFile },
|
||||||
|
pathSupplier = PathSupplier { pathSupplier.invoke() }
|
||||||
|
)
|
||||||
|
|
||||||
|
val file: File by fileSupplier
|
||||||
|
val path: String by pathSupplier
|
||||||
|
|
||||||
private val initialized = AtomicBoolean(false)
|
private val initialized = AtomicBoolean(false)
|
||||||
|
|
||||||
@ -259,15 +203,44 @@ internal enum class AppPaths(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个内部方法, 用于将 [initialized] 状态重置.
|
||||||
|
*
|
||||||
|
* 如果不重置该状态, 将使得单元测试无法让 AppPath 重新初始化文件.
|
||||||
|
*
|
||||||
|
* 警告: 该方法不应该被非测试代码调用.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
private fun reset() {
|
||||||
|
log.warn {
|
||||||
|
"初始化状态已重置: `${this.name}`, 如果在非测试环境中重置状态, 请报告该问题."
|
||||||
|
}
|
||||||
|
initialized.set(false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
private object PathConst {
|
object PathConst {
|
||||||
const val PROP_DATA_PATH = "bot.path.data"
|
const val PROP_DATA_PATH = "bot.path.data"
|
||||||
const val ENV_DATA_PATH = "BOT_DATA_PATH"
|
const val ENV_DATA_PATH = "BOT_DATA_PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FileSupplier(private val supplier: Supplier<File>) {
|
||||||
|
operator fun getValue(appPaths: AppPaths, property: KProperty<*>): File = supplier.get()
|
||||||
|
|
||||||
|
val file: File
|
||||||
|
get() = supplier.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PathSupplier(private val supplier: Supplier<String>) {
|
||||||
|
operator fun getValue(appPaths: AppPaths, property: KProperty<*>): String = supplier.get()
|
||||||
|
|
||||||
|
val path: String
|
||||||
|
get() = supplier.get()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,8 +252,37 @@ internal class LogDirectorySupplier : PropertyDefinerBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class LogLevelSupplier : PropertyDefinerBase() {
|
||||||
|
override fun getPropertyValue(): String {
|
||||||
|
val property = System.getProperty("scalabot.log.level", System.getenv("BOT_LOG_LEVEL"))
|
||||||
|
val level = if (property != null) {
|
||||||
|
try {
|
||||||
|
Level.valueOf(property.uppercase())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
addWarn("Invalid log level: `$property`, the log will be output using the Info log level.")
|
||||||
|
Level.INFO
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Level.INFO
|
||||||
|
}
|
||||||
|
return level.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NetworkVerboseLogSupplier : PropertyDefinerBase() {
|
||||||
|
override fun getPropertyValue(): String {
|
||||||
|
val propertyValue = System.getProperty("scalabot.log.network.verbose", "false")
|
||||||
|
return if (propertyValue.toBoolean()) {
|
||||||
|
"DEBUG"
|
||||||
|
} else {
|
||||||
|
"INFO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal object Const {
|
internal object Const {
|
||||||
val config = loadAppConfig()
|
val config = loadAppConfig()
|
||||||
|
const val METRICS_NAMESPACE = "scalabot"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AppPaths.defaultInitializer() {
|
private fun AppPaths.defaultInitializer() {
|
||||||
@ -298,46 +300,65 @@ private fun AppPaths.defaultInitializer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun initialFiles() {
|
/**
|
||||||
val configFilesNotInitialized = !AppPaths.DEFAULT_CONFIG_APPLICATION.file.exists()
|
* 执行 AppPaths 所有项目的初始化, 并检查是否停止运行, 让用户编辑配置.
|
||||||
&& !AppPaths.DEFAULT_CONFIG_BOT.file.exists()
|
*
|
||||||
|
* @return 如果需要让用户编辑配置, 则返回 `true`.
|
||||||
|
*/
|
||||||
|
internal fun initialFiles(): Boolean {
|
||||||
|
val configFilesNotInitialized = !AppPaths.CONFIG_APPLICATION.file.exists()
|
||||||
|
&& !AppPaths.CONFIG_BOT.file.exists()
|
||||||
|
|
||||||
for (path in AppPaths.values()) {
|
for (path in AppPaths.entries) {
|
||||||
path.initial()
|
path.initial()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configFilesNotInitialized) {
|
if (configFilesNotInitialized) {
|
||||||
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
|
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
|
||||||
exitProcess(1)
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private object GsonConst {
|
internal object GsonConst {
|
||||||
val baseGson: Gson = GsonBuilder()
|
private val baseGson: Gson = GsonBuilder()
|
||||||
.setPrettyPrinting()
|
.setPrettyPrinting()
|
||||||
.serializeNulls()
|
.serializeNulls()
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
val appConfigGson: Gson = baseGson.newBuilder()
|
val appConfigGson: Gson = baseGson.newBuilder()
|
||||||
.registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
|
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||||
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||||
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||||
|
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
val botConfigGson: Gson = baseGson.newBuilder()
|
val botConfigGson: Gson = baseGson.newBuilder()
|
||||||
.registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
|
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||||
|
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||||
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||||
|
.registerTypeAdapter(BotAccount::class.java, BotAccountSerializer)
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun loadAppConfig(configFile: File = AppPaths.DEFAULT_CONFIG_APPLICATION.file): AppConfig {
|
internal fun loadAppConfig(configFile: File = AppPaths.CONFIG_APPLICATION.file): AppConfig {
|
||||||
configFile.bufferedReader(StandardCharsets.UTF_8).use {
|
try {
|
||||||
return GsonConst.appConfigGson.fromJson(it, AppConfig::class.java)!!
|
configFile.bufferedReader(StandardCharsets.UTF_8).use {
|
||||||
|
return GsonConst.appConfigGson.fromJson(it, AppConfig::class.java)!!
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error { "读取 config.json 时发生错误, 请检查配置格式是否正确." }
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun loadBotConfig(botConfigFile: File = AppPaths.DEFAULT_CONFIG_BOT.file): Set<BotConfig> {
|
internal fun loadBotConfigJson(botConfigFile: File = AppPaths.CONFIG_BOT.file): JsonArray? {
|
||||||
botConfigFile.bufferedReader(StandardCharsets.UTF_8).use {
|
try {
|
||||||
return GsonConst.botConfigGson.fromJson(it, object : TypeToken<Set<BotConfig>>() {}.type)!!
|
botConfigFile.bufferedReader(StandardCharsets.UTF_8).use {
|
||||||
|
return GsonConst.botConfigGson.fromJson(it, JsonArray::class.java)!!
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e) { "读取 Bot 配置文件 (bot.json) 时发生错误, 请检查配置格式是否正确." }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
package net.lamgc.scalabot
|
package net.lamgc.scalabot
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
import io.prometheus.client.exporter.HTTPServer
|
import io.prometheus.client.exporter.HTTPServer
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.config.*
|
||||||
import net.lamgc.scalabot.util.registerShutdownHook
|
import net.lamgc.scalabot.util.registerShutdownHook
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import org.eclipse.aether.repository.LocalRepository
|
import org.eclipse.aether.repository.LocalRepository
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient
|
||||||
import org.telegram.telegrambots.meta.TelegramBotsApi
|
import org.telegram.telegrambots.longpolling.BotSession
|
||||||
import org.telegram.telegrambots.meta.generics.BotSession
|
import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication
|
||||||
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession
|
import org.telegram.telegrambots.meta.api.methods.GetMe
|
||||||
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
import java.nio.file.attribute.PosixFilePermissions
|
import java.nio.file.attribute.PosixFilePermissions
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.createDirectories
|
||||||
@ -24,12 +30,15 @@ private val log = KotlinLogging.logger { }
|
|||||||
fun main(args: Array<String>): Unit = runBlocking {
|
fun main(args: Array<String>): Unit = runBlocking {
|
||||||
log.info { "ScalaBot 正在启动中..." }
|
log.info { "ScalaBot 正在启动中..." }
|
||||||
log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
|
log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
|
||||||
|
log.debug { "Kotlin: ${KotlinVersion.CURRENT}, JVM: ${Runtime.version()}" }
|
||||||
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
|
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
|
||||||
initialFiles()
|
if (initialFiles()) {
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
val launcher = Launcher()
|
val launcher = Launcher()
|
||||||
.registerShutdownHook()
|
.registerShutdownHook()
|
||||||
startMetricsServer()
|
startMetricsServer()?.registerShutdownHook()
|
||||||
if (!launcher.launch()) {
|
if (!launcher.launch()) {
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
@ -39,61 +48,60 @@ fun main(args: Array<String>): Unit = runBlocking {
|
|||||||
* 启动运行指标服务器.
|
* 启动运行指标服务器.
|
||||||
* 使用 Prometheus 指标格式.
|
* 使用 Prometheus 指标格式.
|
||||||
*/
|
*/
|
||||||
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) {
|
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics): HTTPServer? {
|
||||||
if (!config.enable) {
|
if (!config.enable) {
|
||||||
log.debug { "运行指标服务器已禁用." }
|
log.debug { "运行指标服务器已禁用." }
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = HTTPServer.Builder()
|
val builder = HTTPServer.Builder()
|
||||||
.withDaemonThreads(true)
|
.withDaemonThreads(true)
|
||||||
|
.withAuthenticator(config.authenticator)
|
||||||
.withPort(config.port)
|
.withPort(config.port)
|
||||||
.withHostname(config.bindAddress)
|
.withHostname(config.bindAddress)
|
||||||
|
|
||||||
val httpServer = builder
|
val httpServer = builder
|
||||||
.build()
|
.build()
|
||||||
.registerShutdownHook()
|
|
||||||
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
|
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
|
||||||
|
return httpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class Launcher(private val config: AppConfig = Const.config) : AutoCloseable {
|
internal class Launcher(
|
||||||
|
private val config: AppConfig = Const.config,
|
||||||
|
private val configFile: File = AppPaths.CONFIG_APPLICATION.file,
|
||||||
|
) : AutoCloseable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private val log = KotlinLogging.logger { }
|
private val log = KotlinLogging.logger { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val botApi = TelegramBotsApi(DefaultBotSession::class.java)
|
private val botApi = TelegramBotsLongPollingApplication()
|
||||||
private val botSessionMap = mutableMapOf<ScalaBot, BotSession>()
|
private val botSessionMap = mutableMapOf<ScalaBot, BotSession>()
|
||||||
private val mavenLocalRepository = getMavenLocalRepository()
|
private val mavenLocalRepository = getMavenLocalRepository()
|
||||||
|
|
||||||
private fun getMavenLocalRepository(): LocalRepository {
|
private fun getMavenLocalRepository(): LocalRepository {
|
||||||
val localPath =
|
val localPath =
|
||||||
if (config.mavenLocalRepository != null && config.mavenLocalRepository.isNotEmpty()) {
|
if (config.mavenLocalRepository != null && config.mavenLocalRepository!!.isNotEmpty()) {
|
||||||
val repoPath = AppPaths.DATA_ROOT.file.toPath()
|
val repoPath = configFile.toPath().resolve(config.mavenLocalRepository!!).apply {
|
||||||
.resolve(config.mavenLocalRepository)
|
if (!exists()) {
|
||||||
.apply {
|
if (!parent.isWritable() || !parent.isReadable()) {
|
||||||
if (!exists()) {
|
throw IOException("Unable to read and write the directory where Maven repository is located.")
|
||||||
if (!parent.isWritable() || !parent.isReadable()) {
|
}
|
||||||
throw IOException("Unable to read and write the directory where Maven repository is located.")
|
if (System.getProperty("os.name").lowercase().startsWith("windows")) {
|
||||||
}
|
createDirectories()
|
||||||
if (System.getProperty("os.name").lowercase().startsWith("windows")) {
|
} else {
|
||||||
createDirectories()
|
val fileAttributes = setOf(
|
||||||
} else {
|
PosixFilePermission.OWNER_READ,
|
||||||
createDirectories(
|
PosixFilePermission.OWNER_WRITE,
|
||||||
PosixFilePermissions.asFileAttribute(
|
PosixFilePermission.GROUP_READ,
|
||||||
setOf(
|
PosixFilePermission.GROUP_WRITE,
|
||||||
PosixFilePermission.OWNER_READ,
|
PosixFilePermission.OTHERS_READ,
|
||||||
PosixFilePermission.OWNER_WRITE,
|
)
|
||||||
PosixFilePermission.GROUP_READ,
|
createDirectories(PosixFilePermissions.asFileAttribute(fileAttributes))
|
||||||
PosixFilePermission.GROUP_WRITE,
|
|
||||||
PosixFilePermission.OTHERS_READ,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.toRealPath()
|
.toRealPath()
|
||||||
.toFile()
|
.toFile()
|
||||||
repoPath
|
repoPath
|
||||||
@ -108,22 +116,47 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun launch(): Boolean {
|
fun launch(): Boolean {
|
||||||
val botConfigs = loadBotConfig()
|
val botConfigs = loadBotConfigJson() ?: return false
|
||||||
if (botConfigs.isEmpty()) {
|
if (botConfigs.isEmpty) {
|
||||||
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
|
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
|
||||||
return false
|
return false
|
||||||
} else if (botConfigs.none { it.enabled }) {
|
|
||||||
log.warn { "配置文件中没有已启用的机器人, 请至少启用一个机器人." }
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
for (botConfig in botConfigs) {
|
var launchedCounts = 0
|
||||||
|
for (botConfigJson in botConfigs) {
|
||||||
|
val botConfig = try {
|
||||||
|
GsonConst.botConfigGson.fromJson(botConfigJson, BotConfig::class.java)
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
val botName = try {
|
||||||
|
botConfigJson.asJsonObject.get("account")?.asJsonObject?.get("name")?.asString ?: "Unknown"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"Unknown"
|
||||||
|
}
|
||||||
|
log.error(e) { "机器人 `$botName` 配置有误, 跳过该机器人的启动." }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
launchBot(botConfig)
|
launchBot(botConfig)
|
||||||
|
launchedCounts++
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "机器人 `${botConfig.account.name}` 启动时发生错误." }
|
if (e is TelegramApiRequestException && e.errorCode == 401) {
|
||||||
|
log.error { "机器人 `${botConfig.account.name}` 的 Bot Token 无效, 请检查配置: [${e.errorCode}] ${e.apiResponse}" }
|
||||||
|
} else {
|
||||||
|
log.error(e) { "机器人 `${botConfig.account.name}` 启动时发生错误." }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
botApi.start()
|
||||||
|
botApi.registerShutdownHook()
|
||||||
|
|
||||||
|
return if (launchedCounts != 0) {
|
||||||
|
log.info { "已启动 $launchedCounts 个机器人." }
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
log.warn { "未启动任何机器人, 请检查配置并至少启用一个机器人." }
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchBot(botConfig: BotConfig) {
|
private fun launchBot(botConfig: BotConfig) {
|
||||||
@ -132,36 +165,38 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
|
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
|
||||||
val botOption = DefaultBotOptions().apply {
|
val proxyConfig =
|
||||||
val proxyConfig =
|
if (botConfig.proxy.type != ProxyType.NO_PROXY) {
|
||||||
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
log.debug { "[Bot ${botConfig.account.name}] 使用独立代理: ${botConfig.proxy.type}" }
|
||||||
botConfig.proxy
|
botConfig.proxy
|
||||||
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
} else if (config.proxy.type != ProxyType.NO_PROXY) {
|
||||||
config.proxy
|
log.debug { "[Bot ${botConfig.account.name}] 使用全局代理: ${botConfig.proxy.type}" }
|
||||||
} else {
|
config.proxy
|
||||||
null
|
} else {
|
||||||
}
|
log.debug { "[Bot ${botConfig.account.name}] 不使用代理." }
|
||||||
if (proxyConfig != null) {
|
ProxyConfig(type = ProxyType.NO_PROXY)
|
||||||
proxyType = proxyConfig.type
|
|
||||||
proxyHost = config.proxy.host
|
|
||||||
proxyPort = config.proxy.port
|
|
||||||
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (botConfig.baseApiUrl != null) {
|
val okhttpClientBuilder = OkHttpClient.Builder()
|
||||||
baseUrl = botConfig.baseApiUrl
|
|
||||||
}
|
if (proxyConfig.type != ProxyType.NO_PROXY) {
|
||||||
|
val proxyType = proxyConfig.type.toJavaProxyType()
|
||||||
|
val proxyAddress = InetSocketAddress.createUnresolved(proxyConfig.host, proxyConfig.port)
|
||||||
|
okhttpClientBuilder.proxy(Proxy(proxyType, proxyAddress))
|
||||||
}
|
}
|
||||||
|
|
||||||
val account = botConfig.account
|
val account = botConfig.account
|
||||||
|
val telegramClient =
|
||||||
|
OkHttpTelegramClient(okhttpClientBuilder.build(), account.token, botConfig.getBaseApiTelegramUrl())
|
||||||
|
|
||||||
val remoteRepositories = config.mavenRepositories
|
val remoteRepositories = config.mavenRepositories
|
||||||
.map { it.toRemoteRepository(config.proxy) }
|
.map { it.toRemoteRepository(proxyConfig) }
|
||||||
.toMutableList().apply {
|
.toMutableList().apply {
|
||||||
if (this.none {
|
if (this.none {
|
||||||
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|
||||||
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
|
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
|
||||||
}) {
|
}) {
|
||||||
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = config.proxy.toAetherProxy()))
|
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = proxyConfig.toAetherProxy()))
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
val extensionPackageFinders = setOf(
|
val extensionPackageFinders = setOf(
|
||||||
@ -174,11 +209,15 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
|||||||
|
|
||||||
val bot = ScalaBot(
|
val bot = ScalaBot(
|
||||||
BotDBMaker.getBotDbInstance(account),
|
BotDBMaker.getBotDbInstance(account),
|
||||||
botOption,
|
telegramClient,
|
||||||
extensionPackageFinders,
|
extensionPackageFinders,
|
||||||
botConfig
|
botConfig
|
||||||
)
|
)
|
||||||
botSessionMap[bot] = botApi.registerBot(bot)
|
|
||||||
|
val botUser = bot.telegramClient.execute(GetMe())
|
||||||
|
log.debug { "已验证 Bot Token 有效性, Bot Username: ${botUser.userName}" }
|
||||||
|
|
||||||
|
botSessionMap[bot] = botApi.registerBot(botConfig.account.token, bot)
|
||||||
log.info { "机器人 `${bot.botUsername}` 已启动." }
|
log.info { "机器人 `${bot.botUsername}` 已启动." }
|
||||||
|
|
||||||
if (botConfig.autoUpdateCommandList) {
|
if (botConfig.autoUpdateCommandList) {
|
||||||
|
@ -2,21 +2,23 @@ package net.lamgc.scalabot
|
|||||||
|
|
||||||
import com.google.common.io.Files
|
import com.google.common.io.Files
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.config.BotAccount
|
||||||
import net.lamgc.scalabot.util.toHexString
|
import net.lamgc.scalabot.util.toHexString
|
||||||
import org.mapdb.DB
|
import org.mapdb.DB
|
||||||
import org.mapdb.DBException
|
import org.mapdb.DBException
|
||||||
import org.mapdb.DBMaker
|
import org.mapdb.DBMaker
|
||||||
import org.telegram.abilitybots.api.db.DBContext
|
import org.telegram.telegrambots.abilitybots.api.db.DBContext
|
||||||
import org.telegram.abilitybots.api.db.MapDBContext
|
import org.telegram.telegrambots.abilitybots.api.db.MapDBContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据库适配器.
|
* 数据库适配器列表.
|
||||||
*
|
|
||||||
* 应按照新到旧的顺序放置, 新的适配器应该在上面.
|
* 应按照新到旧的顺序放置, 新的适配器应该在上面.
|
||||||
|
* @suppress 由于本列表需要设置已弃用的适配器以保证旧版数据库的正常使用, 故忽略弃用警告.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private val adapters = arrayListOf<DbAdapter>(
|
private val adapters = arrayListOf<DbAdapter>(
|
||||||
BotAccountIdDbAdapter, // since [v0.2.0 ~ latest)
|
BotAccountIdDbAdapter, // since [v0.2.0 ~ latest)
|
||||||
BotTokenDbAdapter // since [v0.0.1 ~ v0.2.0)
|
BotTokenDbAdapter // since [v0.0.1 ~ v0.2.0)
|
||||||
@ -155,7 +157,6 @@ private abstract class FileDbAdapter(
|
|||||||
val oldFile = oldDbAdapter.getBotDbFile(botAccount)
|
val oldFile = oldDbAdapter.getBotDbFile(botAccount)
|
||||||
val newFile = getBotDbFile(botAccount)
|
val newFile = getBotDbFile(botAccount)
|
||||||
try {
|
try {
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
Files.copy(oldFile, newFile)
|
Files.copy(oldFile, newFile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (newFile.exists()) {
|
if (newFile.exists()) {
|
||||||
@ -183,6 +184,7 @@ private object BotAccountIdDbAdapter : FileDbAdapter("BotAccountId", { botAccoun
|
|||||||
*
|
*
|
||||||
* **已弃用**: 由于 Token 可以重新生成, 当 Token 改变后数据库文件名也会改变, 故弃用该方法.
|
* **已弃用**: 由于 Token 可以重新生成, 当 Token 改变后数据库文件名也会改变, 故弃用该方法.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(message = "由于 BotToken 可变, 故不再使用该适配器.", level = DeprecationLevel.WARNING)
|
||||||
private object BotTokenDbAdapter : FileDbAdapter("BotToken_v0.1.0", { botAccount ->
|
private object BotTokenDbAdapter : FileDbAdapter("BotToken_v0.1.0", { botAccount ->
|
||||||
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
|
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package net.lamgc.scalabot
|
package net.lamgc.scalabot
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.extension.BotExtensionCreateOptions
|
||||||
import net.lamgc.scalabot.extension.BotExtensionFactory
|
import net.lamgc.scalabot.extension.BotExtensionFactory
|
||||||
import net.lamgc.scalabot.util.getPriority
|
import net.lamgc.scalabot.util.getPriority
|
||||||
import org.eclipse.aether.artifact.Artifact
|
import org.eclipse.aether.artifact.Artifact
|
||||||
import org.telegram.abilitybots.api.util.AbilityExtension
|
import org.telegram.telegrambots.abilitybots.api.util.AbilityExtension
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@ -14,6 +15,18 @@ import java.nio.charset.StandardCharsets
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展加载器.
|
||||||
|
*
|
||||||
|
* 扩展加载器并非负责加载扩展的 Class, 而是委派搜索器发现并获取扩展, 然后加载扩展实例.
|
||||||
|
*
|
||||||
|
* 注意, 扩展加载器将内置两个 Finder: [FileNameFinder] 和 [MavenMetaInformationFinder].
|
||||||
|
*
|
||||||
|
* @param bot 扩展加载器所负责的 ScalaBot 实例.
|
||||||
|
* @param extensionsDataFolder 提供给扩展用于数据存储的根目录(实际目录为 `{root}/{group...}/{artifact}`).
|
||||||
|
* @param extensionsPath 提供给 Finder 用于搜索扩展的本地扩展包存放路径.
|
||||||
|
* @param extensionFinders 加载器所使用的搜索器集合. 加载扩展时将使用所提供的的加载器.
|
||||||
|
*/
|
||||||
internal class ExtensionLoader(
|
internal class ExtensionLoader(
|
||||||
private val bot: ScalaBot,
|
private val bot: ScalaBot,
|
||||||
private val extensionsDataFolder: File = AppPaths.DATA_EXTENSIONS.file,
|
private val extensionsDataFolder: File = AppPaths.DATA_EXTENSIONS.file,
|
||||||
@ -27,6 +40,13 @@ internal class ExtensionLoader(
|
|||||||
MavenMetaInformationFinder
|
MavenMetaInformationFinder
|
||||||
).apply { addAll(extensionFinders) }.toSet()
|
).apply { addAll(extensionFinders) }.toSet()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载扩展, 并返回扩展项.
|
||||||
|
*
|
||||||
|
* 调用本方法后, 将会指派提供的 Finder 搜索 ScalaBot 配置的扩展包.
|
||||||
|
*
|
||||||
|
* @return 返回存放了所有已加载扩展项的 Set. 可通过 [LoadedExtensionEntry] 获取扩展的有关信息.
|
||||||
|
*/
|
||||||
fun getExtensions(): Set<LoadedExtensionEntry> {
|
fun getExtensions(): Set<LoadedExtensionEntry> {
|
||||||
val extensionEntries = mutableSetOf<LoadedExtensionEntry>()
|
val extensionEntries = mutableSetOf<LoadedExtensionEntry>()
|
||||||
for (extensionArtifact in bot.extensions) {
|
for (extensionArtifact in bot.extensions) {
|
||||||
@ -52,6 +72,17 @@ internal class ExtensionLoader(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否发生冲突.
|
* 检查是否发生冲突.
|
||||||
|
*
|
||||||
|
* 扩展包冲突有两种情况:
|
||||||
|
* 1. 有多个同为最高优先级的搜索器搜索到了扩展包.
|
||||||
|
* 2. 唯一的最高优先级搜索器搜索到了多个扩展包.
|
||||||
|
*
|
||||||
|
* 扩展包冲突指的是**有多个具有相同构件坐标的扩展包被搜索到**,
|
||||||
|
* 如果不顾扩展包冲突直接加载的话, 将会出现安全隐患,
|
||||||
|
* 因此在加载器发现冲突的情况下将输出相关信息, 提示用户进行排查.
|
||||||
|
*
|
||||||
|
* @param foundResult 扩展包搜索结果.
|
||||||
|
*
|
||||||
* @return 如果出现冲突, 返回 `true`.
|
* @return 如果出现冲突, 返回 `true`.
|
||||||
*/
|
*/
|
||||||
private fun checkConflict(foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Boolean {
|
private fun checkConflict(foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Boolean {
|
||||||
@ -68,6 +99,9 @@ internal class ExtensionLoader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从结果中过滤出由最高优先级的搜索器搜索到的扩展包.
|
||||||
|
*/
|
||||||
private fun filterHighPriorityResult(foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>)
|
private fun filterHighPriorityResult(foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>)
|
||||||
: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>> {
|
: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>> {
|
||||||
val finders: List<ExtensionPackageFinder> = foundResult.keys
|
val finders: List<ExtensionPackageFinder> = foundResult.keys
|
||||||
@ -89,7 +123,13 @@ internal class ExtensionLoader(
|
|||||||
for (factory in extClassLoader.serviceLoader) {
|
for (factory in extClassLoader.serviceLoader) {
|
||||||
try {
|
try {
|
||||||
val extension =
|
val extension =
|
||||||
factory.createExtensionInstance(bot, getExtensionDataFolder(extensionArtifact))
|
factory.createExtensionInstance(
|
||||||
|
bot, getExtensionDataFolder(extensionArtifact),
|
||||||
|
BotExtensionCreateOptions(
|
||||||
|
bot.accountId,
|
||||||
|
bot.botConfig.proxy.copy()
|
||||||
|
)
|
||||||
|
)
|
||||||
if (extension == null) {
|
if (extension == null) {
|
||||||
log.debug { "Factory ${factory::class.java} 创建插件时返回了 null, 已跳过. (BotName: ${bot.botUsername})" }
|
log.debug { "Factory ${factory::class.java} 创建插件时返回了 null, 已跳过. (BotName: ${bot.botUsername})" }
|
||||||
continue
|
continue
|
||||||
@ -102,6 +142,11 @@ internal class ExtensionLoader(
|
|||||||
return factories.toSet()
|
return factories.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只是用来统计扩展包搜索结果的数量而已.
|
||||||
|
*
|
||||||
|
* @return 返回扩展包的数量.
|
||||||
|
*/
|
||||||
private fun allFoundedPackageNumber(filesMap: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Int {
|
private fun allFoundedPackageNumber(filesMap: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Int {
|
||||||
var number = 0
|
var number = 0
|
||||||
for (files in filesMap.values) {
|
for (files in filesMap.values) {
|
||||||
@ -110,6 +155,14 @@ internal class ExtensionLoader(
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索指定构件坐标的依赖包.
|
||||||
|
*
|
||||||
|
* 搜索扩展包将根据搜索器优先级从高到低依次搜索, 当某一个优先级的搜索器搜到扩展包后将停止搜索.
|
||||||
|
* 可以根据不同优先级的搜索器, 配置扩展包的主用和备用文件.
|
||||||
|
*
|
||||||
|
* @return 返回各个搜索器返回的搜索结果.
|
||||||
|
*/
|
||||||
private fun findExtensionPackage(
|
private fun findExtensionPackage(
|
||||||
extensionArtifact: Artifact,
|
extensionArtifact: Artifact,
|
||||||
): Map<ExtensionPackageFinder, Set<FoundExtensionPackage>> {
|
): Map<ExtensionPackageFinder, Set<FoundExtensionPackage>> {
|
||||||
@ -138,31 +191,45 @@ internal class ExtensionLoader(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查扩展包搜索器是否设置了 [FinderRules] 注解.
|
||||||
|
* @return 如果已设置注解, 则返回 `true`.
|
||||||
|
*/
|
||||||
private fun checkExtensionPackageFinder(finder: ExtensionPackageFinder): Boolean =
|
private fun checkExtensionPackageFinder(finder: ExtensionPackageFinder): Boolean =
|
||||||
finder::class.java.getDeclaredAnnotation(FinderRules::class.java) != null
|
finder::class.java.getDeclaredAnnotation(FinderRules::class.java) != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在日志中输出有关扩展包冲突的错误信息.
|
||||||
|
*/
|
||||||
private fun printExtensionFileConflictError(
|
private fun printExtensionFileConflictError(
|
||||||
extensionArtifact: Artifact,
|
extensionArtifact: Artifact,
|
||||||
foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>
|
foundResult: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>
|
||||||
) {
|
) {
|
||||||
val errMessage = StringBuilder(
|
log.error {
|
||||||
"""
|
val errMessage = StringBuilder(
|
||||||
[Bot ${bot.botUsername}] 扩展包 $extensionArtifact 存在多个文件, 为防止安全问题, 已禁止加载该扩展包:
|
"""
|
||||||
""".trimIndent()
|
[Bot ${bot.botUsername}] 扩展包 $extensionArtifact 存在多个文件, 为防止安全问题, 已禁止加载该扩展包:
|
||||||
).append('\n')
|
""".trimIndent()
|
||||||
|
).append('\n')
|
||||||
|
|
||||||
foundResult.forEach { (finder, files) ->
|
foundResult.forEach { (finder, files) ->
|
||||||
errMessage.append("\t- 搜索器 `").append(finder::class.simpleName).append("`")
|
errMessage.append("\t- 搜索器 `").append(finder::class.simpleName).append("`")
|
||||||
.append("(Priority: ${finder.getPriority()})")
|
.append("(Priority: ${finder.getPriority()})")
|
||||||
.append(" 找到了以下扩展包: \n")
|
.append(" 找到了以下扩展包: \n")
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
errMessage.append("\t\t* ")
|
errMessage.append("\t\t* ")
|
||||||
.append(URLDecoder.decode(file.getRawUrl().toString(), StandardCharsets.UTF_8)).append('\n')
|
.append(URLDecoder.decode(file.getRawUrl().toString(), StandardCharsets.UTF_8)).append('\n')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
errMessage
|
||||||
}
|
}
|
||||||
log.error { errMessage }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建扩展数据目录, 并返回 [File] 对象.
|
||||||
|
* @param extensionArtifact 扩展包构件坐标.
|
||||||
|
* @return 返回对应的数据存储目录.
|
||||||
|
*/
|
||||||
private fun getExtensionDataFolder(extensionArtifact: Artifact): File {
|
private fun getExtensionDataFolder(extensionArtifact: Artifact): File {
|
||||||
val dataFolder =
|
val dataFolder =
|
||||||
File(extensionsDataFolder, "${extensionArtifact.groupId}/${extensionArtifact.artifactId}")
|
File(extensionsDataFolder, "${extensionArtifact.groupId}/${extensionArtifact.artifactId}")
|
||||||
@ -172,6 +239,12 @@ internal class ExtensionLoader(
|
|||||||
return dataFolder
|
return dataFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已加载扩展项.
|
||||||
|
* @property extensionArtifact 扩展的构件坐标([Artifact]).
|
||||||
|
* @property factoryClass 扩展的工厂类.
|
||||||
|
* @property extension 扩展实例.
|
||||||
|
*/
|
||||||
data class LoadedExtensionEntry(
|
data class LoadedExtensionEntry(
|
||||||
val extensionArtifact: Artifact,
|
val extensionArtifact: Artifact,
|
||||||
val factoryClass: Class<out BotExtensionFactory>,
|
val factoryClass: Class<out BotExtensionFactory>,
|
||||||
@ -181,6 +254,10 @@ internal class ExtensionLoader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 扩展的类加载器清除器.
|
||||||
|
*
|
||||||
|
* 原计划是用来通过关闭 ClassLoader 来卸载扩展的, 但似乎并没有这么做.
|
||||||
|
*
|
||||||
* 该类为保留措施, 尚未启用.
|
* 该类为保留措施, 尚未启用.
|
||||||
*/
|
*/
|
||||||
internal object ExtensionClassLoaderCleaner {
|
internal object ExtensionClassLoaderCleaner {
|
||||||
@ -257,7 +334,7 @@ internal interface ExtensionPackageFinder {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 已找到的扩展包信息.
|
* 已找到的扩展包信息.
|
||||||
* 通过实现该接口, 以寻找远端文件的 [ExtensionPackageFinder]
|
* 通过实现该接口, 以寻找远端文件的 [ExtensionPackageFinder];
|
||||||
* 可以在适当的时候将扩展包下载到本地, 而无需在搜索阶段下载扩展包.
|
* 可以在适当的时候将扩展包下载到本地, 而无需在搜索阶段下载扩展包.
|
||||||
*/
|
*/
|
||||||
internal interface FoundExtensionPackage {
|
internal interface FoundExtensionPackage {
|
||||||
@ -296,6 +373,7 @@ private fun FoundExtensionPackage.createClassLoader(): ExtensionClassLoader =
|
|||||||
* 已找到的扩展包文件.
|
* 已找到的扩展包文件.
|
||||||
* @param artifact 扩展包构件坐标.
|
* @param artifact 扩展包构件坐标.
|
||||||
* @param file 已找到的扩展包文件.
|
* @param file 已找到的扩展包文件.
|
||||||
|
* @param finder 搜索到该扩展包的搜索器.
|
||||||
*/
|
*/
|
||||||
internal class FileFoundExtensionPackage(
|
internal class FileFoundExtensionPackage(
|
||||||
private val artifact: Artifact,
|
private val artifact: Artifact,
|
||||||
@ -342,14 +420,14 @@ internal class ExtensionClassLoader(urls: Array<URL>, dependencyLoader: ClassLoa
|
|||||||
// 以免使用了不来自扩展包的机器人扩展.
|
// 以免使用了不来自扩展包的机器人扩展.
|
||||||
|
|
||||||
override fun getResources(name: String?): Enumeration<URL> {
|
override fun getResources(name: String?): Enumeration<URL> {
|
||||||
if (BotExtensionFactory::class.java.equals(name)) {
|
if ("META-INF/services/${BotExtensionFactory::class.java.name}" == name) {
|
||||||
return findResources(name)
|
return findResources(name)
|
||||||
}
|
}
|
||||||
return super.getResources(name)
|
return super.getResources(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResource(name: String?): URL? {
|
override fun getResource(name: String?): URL? {
|
||||||
if (BotExtensionFactory::class.java.equals(name)) {
|
if ("META-INF/services/${BotExtensionFactory::class.java}" == name) {
|
||||||
return findResource(name)
|
return findResource(name)
|
||||||
}
|
}
|
||||||
return super.getResource(name)
|
return super.getResource(name)
|
||||||
|
@ -26,6 +26,7 @@ import org.jdom2.input.SAXBuilder
|
|||||||
import org.jdom2.xpath.XPathFactory
|
import org.jdom2.xpath.XPathFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.net.URI
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -257,23 +258,25 @@ internal class MavenRepositoryExtensionFinder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
||||||
|
val repositories = repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories).toList()
|
||||||
log.debug {
|
log.debug {
|
||||||
StringBuilder().apply {
|
StringBuilder().apply {
|
||||||
append("构件 $extensionArtifact 将在以下仓库拉取: \n")
|
append("构件 $extensionArtifact 将在以下仓库拉取: \n")
|
||||||
remoteRepositories.forEach {
|
repositories.forEach {
|
||||||
append("\t- ${it}\n")
|
append("\t- $it\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionArtifactResult = repositorySystem.resolveArtifact(
|
val extensionArtifactResult = repositorySystem.resolveArtifact(
|
||||||
repoSystemSession,
|
repoSystemSession,
|
||||||
ArtifactRequest(
|
ArtifactRequest(
|
||||||
extensionArtifact,
|
extensionArtifact,
|
||||||
repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories),
|
repositories,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val extResolvedArtifact = extensionArtifactResult.artifact
|
val resolvedArtifact: Artifact? = extensionArtifactResult.artifact
|
||||||
if (!extensionArtifactResult.isResolved) {
|
if (!extensionArtifactResult.isResolved) {
|
||||||
if (extensionArtifactResult.isMissing) {
|
if (extensionArtifactResult.isMissing) {
|
||||||
log.warn { "在指定的仓库中找不到构件: ${extensionArtifactResult.artifact}" }
|
log.warn { "在指定的仓库中找不到构件: ${extensionArtifactResult.artifact}" }
|
||||||
@ -281,17 +284,26 @@ internal class MavenRepositoryExtensionFinder(
|
|||||||
printArtifactResultExceptions(extensionArtifactResult.exceptions)
|
printArtifactResultExceptions(extensionArtifactResult.exceptions)
|
||||||
}
|
}
|
||||||
return emptySet()
|
return emptySet()
|
||||||
|
} else if (resolvedArtifact == null) {
|
||||||
|
log.warn { "无法在指定的仓库中解析构件: $extensionArtifact" }
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info {
|
||||||
|
"已从 Maven 仓库 `${extensionArtifactResult.repository.id}` 中找到" +
|
||||||
|
"扩展包 `${resolvedArtifact.groupId}:${resolvedArtifact.artifactId}` " +
|
||||||
|
"版本号 `${resolvedArtifact.version}`."
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = DependencyRequest(
|
val request = DependencyRequest(
|
||||||
CollectRequest(Dependency(extResolvedArtifact, null), remoteRepositories),
|
CollectRequest(Dependency(resolvedArtifact, null), repositories),
|
||||||
ScopeDependencyFilter(setOf("runtime", "compile", "provided"), null)
|
ScopeDependencyFilter(setOf("runtime", "compile", "provided"), null)
|
||||||
)
|
)
|
||||||
val dependencyResult = repositorySystem.resolveDependencies(repoSystemSession, request)
|
val dependencyResult = repositorySystem.resolveDependencies(repoSystemSession, request)
|
||||||
val dependencies = checkAndCollectDependencyArtifacts(extensionArtifact, dependencyResult.artifactResults)
|
val dependencies = checkAndCollectDependencyArtifacts(extensionArtifact, dependencyResult.artifactResults)
|
||||||
?: return emptySet()
|
?: return emptySet()
|
||||||
|
|
||||||
return setOf(MavenExtensionPackage(this, extResolvedArtifact, extensionArtifactResult.repository, dependencies))
|
return setOf(MavenExtensionPackage(this, resolvedArtifact, extensionArtifactResult.repository, dependencies))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAndCollectDependencyArtifacts(
|
private fun checkAndCollectDependencyArtifacts(
|
||||||
@ -385,7 +397,6 @@ internal class MavenRepositoryExtensionFinder(
|
|||||||
/**
|
/**
|
||||||
* Maven 中央仓库 Url.
|
* Maven 中央仓库 Url.
|
||||||
*/
|
*/
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
|
||||||
const val MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2/"
|
const val MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2/"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -455,17 +466,19 @@ internal class MavenRepositoryExtensionFinder(
|
|||||||
throw IllegalArgumentException("Unsupported FoundExtensionPackage type: $foundExtensionPackage")
|
throw IllegalArgumentException("Unsupported FoundExtensionPackage type: $foundExtensionPackage")
|
||||||
}
|
}
|
||||||
|
|
||||||
val urls = mutableSetOf<URL>()
|
val urls = mutableSetOf<URI>()
|
||||||
for (dependency in foundExtensionPackage.dependencies) {
|
for (dependency in foundExtensionPackage.dependencies) {
|
||||||
val dependencyFile = dependency.file ?: continue
|
val dependencyFile = dependency.file ?: continue
|
||||||
urls.add(dependencyFile.toURI().toURL())
|
urls.add(dependencyFile.toURI())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将依赖的 ClassLoader 与 ExtensionPackage 的 ClassLoader 分开
|
// 将依赖的 ClassLoader 与 ExtensionPackage 的 ClassLoader 分开
|
||||||
// 这么做可以防范依赖中隐藏的 SPI 注册, 避免安全隐患.
|
// 这么做可以防范依赖中隐藏的 SPI 注册, 避免安全隐患.
|
||||||
|
|
||||||
val dependenciesUrlArray = urls.toTypedArray()
|
val dependenciesUrlArray = urls.toTypedArray()
|
||||||
val dependenciesClassLoader = URLClassLoader(dependenciesUrlArray)
|
val dependenciesClassLoader = URLClassLoader(
|
||||||
|
dependenciesUrlArray.map { it.toURL() }.toTypedArray()
|
||||||
|
)
|
||||||
|
|
||||||
return ExtensionClassLoader(
|
return ExtensionClassLoader(
|
||||||
arrayOf(foundExtensionPackage.getPackageFile().toURI().toURL()),
|
arrayOf(foundExtensionPackage.getPackageFile().toURI().toURL()),
|
||||||
|
@ -4,44 +4,44 @@ import io.prometheus.client.Counter
|
|||||||
import io.prometheus.client.Gauge
|
import io.prometheus.client.Gauge
|
||||||
import io.prometheus.client.Summary
|
import io.prometheus.client.Summary
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.config.BotConfig
|
||||||
import org.eclipse.aether.artifact.Artifact
|
import org.eclipse.aether.artifact.Artifact
|
||||||
import org.telegram.abilitybots.api.bot.AbilityBot
|
import org.telegram.telegrambots.abilitybots.api.bot.AbilityBot
|
||||||
import org.telegram.abilitybots.api.db.DBContext
|
import org.telegram.telegrambots.abilitybots.api.db.DBContext
|
||||||
import org.telegram.abilitybots.api.objects.Ability
|
import org.telegram.telegrambots.abilitybots.api.objects.Ability
|
||||||
import org.telegram.abilitybots.api.toggle.BareboneToggle
|
import org.telegram.telegrambots.abilitybots.api.toggle.BareboneToggle
|
||||||
import org.telegram.abilitybots.api.toggle.DefaultToggle
|
import org.telegram.telegrambots.abilitybots.api.toggle.DefaultToggle
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
|
||||||
import org.telegram.telegrambots.meta.api.methods.commands.DeleteMyCommands
|
import org.telegram.telegrambots.meta.api.methods.commands.DeleteMyCommands
|
||||||
import org.telegram.telegrambots.meta.api.methods.commands.SetMyCommands
|
import org.telegram.telegrambots.meta.api.methods.commands.SetMyCommands
|
||||||
import org.telegram.telegrambots.meta.api.objects.Update
|
import org.telegram.telegrambots.meta.api.objects.Update
|
||||||
import org.telegram.telegrambots.meta.api.objects.commands.BotCommand
|
import org.telegram.telegrambots.meta.api.objects.commands.BotCommand
|
||||||
|
import org.telegram.telegrambots.meta.generics.TelegramClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可扩展 Bot.
|
* 可扩展 Bot.
|
||||||
* @property creatorId 机器人所有人的 Telegram 用户 Id. 可通过联系部分机器人来获取该信息.
|
* @property creatorId 机器人所有人的 Telegram 用户 ID. 可通过联系部分机器人来获取该信息.
|
||||||
* (e.g. [@userinfobot](http://t.me/userinfobot))
|
* (e.g. [@userinfobot](http://t.me/userinfobot))
|
||||||
* @param db 机器人数据库对象. 用于状态机等用途.
|
* @param db 机器人数据库对象. 用于状态机等用途.
|
||||||
* @param options AbilityBot 设置对象.
|
|
||||||
* @property extensions 扩展坐标集合.
|
* @property extensions 扩展坐标集合.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
|
||||||
internal class ScalaBot(
|
internal class ScalaBot(
|
||||||
db: DBContext,
|
db: DBContext,
|
||||||
options: DefaultBotOptions,
|
client: TelegramClient,
|
||||||
extensionFinders: Set<ExtensionPackageFinder>,
|
extensionFinders: Set<ExtensionPackageFinder>,
|
||||||
botConfig: BotConfig,
|
val botConfig: BotConfig,
|
||||||
private val creatorId: Long = botConfig.account.creatorId,
|
|
||||||
val accountId: Long = botConfig.account.id,
|
val accountId: Long = botConfig.account.id,
|
||||||
|
private val creatorId: Long = botConfig.account.creatorId,
|
||||||
val extensions: Set<Artifact> = botConfig.extensions
|
val extensions: Set<Artifact> = botConfig.extensions
|
||||||
) :
|
) :
|
||||||
AbilityBot(
|
AbilityBot(
|
||||||
botConfig.account.token,
|
client,
|
||||||
botConfig.account.name,
|
botConfig.account.name,
|
||||||
db,
|
db,
|
||||||
if (botConfig.disableBuiltInAbility)
|
if (botConfig.disableBuiltInAbility)
|
||||||
BareboneToggle()
|
BareboneToggle()
|
||||||
else
|
else
|
||||||
DefaultToggle(),
|
DefaultToggle()
|
||||||
options
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val extensionLoader = ExtensionLoader(
|
private val extensionLoader = ExtensionLoader(
|
||||||
@ -49,6 +49,8 @@ internal class ScalaBot(
|
|||||||
extensionFinders = extensionFinders
|
extensionFinders = extensionFinders
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val accountIdString = accountId.toString()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
log.info { "[Bot $botUsername] 正在加载扩展..." }
|
log.info { "[Bot $botUsername] 正在加载扩展..." }
|
||||||
val extensionEntries = extensionLoader.getExtensions()
|
val extensionEntries = extensionLoader.getExtensions()
|
||||||
@ -64,19 +66,19 @@ internal class ScalaBot(
|
|||||||
|
|
||||||
override fun creatorId(): Long = creatorId
|
override fun creatorId(): Long = creatorId
|
||||||
|
|
||||||
override fun onUpdateReceived(update: Update?) {
|
override fun consume(update: Update?) {
|
||||||
botUpdateCounter.labels(botUsername).inc()
|
botUpdateCounter.labels(botUsername, accountIdString).inc()
|
||||||
botUpdateGauge.labels(botUsername).inc()
|
botUpdateGauge.labels(botUsername, accountIdString).inc()
|
||||||
|
|
||||||
val timer = updateProcessTime.labels(botUsername).startTimer()
|
val timer = updateProcessTime.labels(botUsername, accountIdString).startTimer()
|
||||||
try {
|
try {
|
||||||
super.onUpdateReceived(update)
|
super.consume(update)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
exceptionHandlingCounter.labels(botUsername).inc()
|
exceptionHandlingCounter.labels(botUsername, accountIdString).inc()
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
timer.observeDuration()
|
timer.observeDuration()
|
||||||
botUpdateGauge.labels(botUsername).dec()
|
botUpdateGauge.labels(botUsername, accountIdString).dec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,11 +91,11 @@ internal class ScalaBot(
|
|||||||
* @return 更新成功返回 `true`.
|
* @return 更新成功返回 `true`.
|
||||||
*/
|
*/
|
||||||
fun updateCommandList(): Boolean {
|
fun updateCommandList(): Boolean {
|
||||||
if (abilities() == null) {
|
if (abilities == null) {
|
||||||
throw IllegalStateException("Abilities has not been initialized.")
|
throw IllegalStateException("Abilities has not been initialized.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val botCommands = abilities().values.map {
|
val botCommands = abilities.values.map {
|
||||||
val abilityInfo = if (it.info() == null || it.info().trim().isEmpty()) {
|
val abilityInfo = if (it.info() == null || it.info().trim().isEmpty()) {
|
||||||
log.warn { "[Bot $botUsername] Ability `${it.name()}` 没有说明信息." }
|
log.warn { "[Bot $botUsername] Ability `${it.name()}` 没有说明信息." }
|
||||||
"(The command has no description)"
|
"(The command has no description)"
|
||||||
@ -109,9 +111,10 @@ internal class ScalaBot(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val setMyCommands = SetMyCommands()
|
val setMyCommands = SetMyCommands.builder()
|
||||||
setMyCommands.commands = botCommands
|
.commands(botCommands)
|
||||||
return execute(DeleteMyCommands()) && execute(setMyCommands)
|
.build()
|
||||||
|
return telegramClient.execute(DeleteMyCommands()) && telegramClient.execute(setMyCommands)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRegister() {
|
override fun onRegister() {
|
||||||
@ -119,10 +122,6 @@ internal class ScalaBot(
|
|||||||
onlineBotGauge.inc()
|
onlineBotGauge.inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosing() {
|
|
||||||
onlineBotGauge.dec()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private val log = KotlinLogging.logger { }
|
private val log = KotlinLogging.logger { }
|
||||||
@ -133,7 +132,8 @@ internal class ScalaBot(
|
|||||||
private val botUpdateCounter = Counter.build()
|
private val botUpdateCounter = Counter.build()
|
||||||
.name("updates_total")
|
.name("updates_total")
|
||||||
.help("Total number of updates received by all bots.")
|
.help("Total number of updates received by all bots.")
|
||||||
.labelNames("bot_name")
|
.labelNames("bot_name", "bot_id")
|
||||||
|
.namespace(Const.METRICS_NAMESPACE)
|
||||||
.subsystem("telegrambots")
|
.subsystem("telegrambots")
|
||||||
.register()
|
.register()
|
||||||
|
|
||||||
@ -141,7 +141,8 @@ internal class ScalaBot(
|
|||||||
private val botUpdateGauge = Gauge.build()
|
private val botUpdateGauge = Gauge.build()
|
||||||
.name("updates_in_progress")
|
.name("updates_in_progress")
|
||||||
.help("Number of updates in process by all bots.")
|
.help("Number of updates in process by all bots.")
|
||||||
.labelNames("bot_name")
|
.labelNames("bot_name", "bot_id")
|
||||||
|
.namespace(Const.METRICS_NAMESPACE)
|
||||||
.subsystem("telegrambots")
|
.subsystem("telegrambots")
|
||||||
.register()
|
.register()
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ internal class ScalaBot(
|
|||||||
private val onlineBotGauge = Gauge.build()
|
private val onlineBotGauge = Gauge.build()
|
||||||
.name("bots_online")
|
.name("bots_online")
|
||||||
.help("Number of bots Online.")
|
.help("Number of bots Online.")
|
||||||
|
.namespace(Const.METRICS_NAMESPACE)
|
||||||
.subsystem("telegrambots")
|
.subsystem("telegrambots")
|
||||||
.register()
|
.register()
|
||||||
|
|
||||||
@ -160,7 +162,8 @@ internal class ScalaBot(
|
|||||||
"so it may be different from the actual execution time of ability. " +
|
"so it may be different from the actual execution time of ability. " +
|
||||||
"It is not recommended to use it as the accurate execution time of ability)"
|
"It is not recommended to use it as the accurate execution time of ability)"
|
||||||
)
|
)
|
||||||
.labelNames("bot_name")
|
.labelNames("bot_name", "bot_id")
|
||||||
|
.namespace(Const.METRICS_NAMESPACE)
|
||||||
.subsystem("telegrambots")
|
.subsystem("telegrambots")
|
||||||
.register()
|
.register()
|
||||||
|
|
||||||
@ -168,7 +171,8 @@ internal class ScalaBot(
|
|||||||
private val exceptionHandlingCounter = Counter.build()
|
private val exceptionHandlingCounter = Counter.build()
|
||||||
.name("updates_exception_handling")
|
.name("updates_exception_handling")
|
||||||
.help("Number of exceptions during processing.")
|
.help("Number of exceptions during processing.")
|
||||||
.labelNames("bot_name")
|
.labelNames("bot_name", "bot_id")
|
||||||
|
.namespace(Const.METRICS_NAMESPACE)
|
||||||
.subsystem("telegrambots")
|
.subsystem("telegrambots")
|
||||||
.register()
|
.register()
|
||||||
}
|
}
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
package net.lamgc.scalabot.util
|
|
||||||
|
|
||||||
import com.google.gson.*
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import net.lamgc.scalabot.MavenRepositoryConfig
|
|
||||||
import org.eclipse.aether.artifact.Artifact
|
|
||||||
import org.eclipse.aether.artifact.DefaultArtifact
|
|
||||||
import org.eclipse.aether.repository.Authentication
|
|
||||||
import org.eclipse.aether.repository.Proxy
|
|
||||||
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>,
|
|
||||||
JsonSerializer<DefaultBotOptions.ProxyType> {
|
|
||||||
|
|
||||||
override fun deserialize(
|
|
||||||
json: JsonElement,
|
|
||||||
typeOfT: Type?,
|
|
||||||
context: JsonDeserializationContext?
|
|
||||||
): DefaultBotOptions.ProxyType {
|
|
||||||
if (!json.isJsonPrimitive) {
|
|
||||||
throw JsonParseException("Wrong configuration value type.")
|
|
||||||
}
|
|
||||||
val value = json.asString.trim()
|
|
||||||
try {
|
|
||||||
return DefaultBotOptions.ProxyType.valueOf(value.uppercase())
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw JsonParseException("Invalid value: $value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serialize(
|
|
||||||
src: DefaultBotOptions.ProxyType,
|
|
||||||
typeOfSrc: Type?,
|
|
||||||
context: JsonSerializationContext?
|
|
||||||
): JsonElement {
|
|
||||||
return JsonPrimitive(src.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
|
|
||||||
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
|
||||||
val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}")
|
|
||||||
if (!src.extension.equals("jar")) {
|
|
||||||
gavBuilder.append(':').append(src.extension)
|
|
||||||
}
|
|
||||||
if (src.classifier.isNotEmpty()) {
|
|
||||||
gavBuilder.append(':').append(src.classifier)
|
|
||||||
}
|
|
||||||
return JsonPrimitive(gavBuilder.append(':').append(src.version).toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
|
|
||||||
if (!json!!.isJsonPrimitive) {
|
|
||||||
throw JsonParseException("Wrong configuration value type.")
|
|
||||||
}
|
|
||||||
return DefaultArtifact(json.asString.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object AuthenticationSerializer : JsonDeserializer<Authentication> {
|
|
||||||
|
|
||||||
private val log = KotlinLogging.logger { }
|
|
||||||
|
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication? {
|
|
||||||
val builder = AuthenticationBuilder()
|
|
||||||
when (json) {
|
|
||||||
is JsonArray -> {
|
|
||||||
for (element in json) {
|
|
||||||
if (element is JsonArray) {
|
|
||||||
builder.addCustom(jsonArrayToAuthentication(element))
|
|
||||||
} else if (element is JsonObject) {
|
|
||||||
jsonToAuthentication(element, builder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is JsonObject -> {
|
|
||||||
jsonToAuthentication(json, builder)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw JsonParseException("Unsupported JSON data type: ${json::class.java}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun jsonArrayToAuthentication(jsonArray: JsonArray): Authentication {
|
|
||||||
val builder = AuthenticationBuilder()
|
|
||||||
for (element in jsonArray) {
|
|
||||||
when (element) {
|
|
||||||
is JsonObject -> jsonToAuthentication(element, builder)
|
|
||||||
is JsonArray -> builder.addCustom(jsonArrayToAuthentication(element))
|
|
||||||
else -> log.warn { "不支持的 Json 类型: ${element::class.java}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val KEY_TYPE = "type"
|
|
||||||
|
|
||||||
private fun jsonToAuthentication(json: JsonObject, builder: AuthenticationBuilder) {
|
|
||||||
if (!json.has(KEY_TYPE)) {
|
|
||||||
log.warn { "缺少 type 字段, 无法判断 Maven 认证信息类型." }
|
|
||||||
return
|
|
||||||
} else if (!json.get(KEY_TYPE).isJsonPrimitive) {
|
|
||||||
log.warn { "type 字段类型错误(应为 Primitive 类型), 无法判断 Maven 认证信息类型.(实际类型: `${json::class.java}`)" }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (json.get(KEY_TYPE).asString.trim().lowercase()) {
|
|
||||||
"string" -> {
|
|
||||||
builder.addString(checkJsonKey(json, "key"), checkJsonKey(json, "value"))
|
|
||||||
}
|
|
||||||
"secret" -> {
|
|
||||||
builder.addSecret(checkJsonKey(json, "key"), checkJsonKey(json, "value"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkJsonKey(json: JsonObject, key: String): String {
|
|
||||||
if (!json.has(key)) {
|
|
||||||
throw JsonParseException("Required field does not exist: $key")
|
|
||||||
} else if (!json.get(key).isJsonPrimitive) {
|
|
||||||
throw JsonParseException("Wrong field `$key` type: ${json.get(key)::class.java}")
|
|
||||||
}
|
|
||||||
return json.get(key).asString
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object MavenRepositoryConfigSerializer
|
|
||||||
: JsonDeserializer<MavenRepositoryConfig> {
|
|
||||||
|
|
||||||
override fun deserialize(
|
|
||||||
json: JsonElement,
|
|
||||||
typeOfT: Type,
|
|
||||||
context: JsonDeserializationContext
|
|
||||||
): MavenRepositoryConfig {
|
|
||||||
return when (json) {
|
|
||||||
is JsonObject -> {
|
|
||||||
MavenRepositoryConfig(
|
|
||||||
id = json.get("id")?.asString,
|
|
||||||
url = URL(checkJsonKey(json, "url")),
|
|
||||||
proxy = if (json.has("proxy") && json.get("proxy").isJsonObject)
|
|
||||||
context.deserialize<Proxy>(
|
|
||||||
json.getAsJsonObject("proxy"), Proxy::class.java
|
|
||||||
) else null,
|
|
||||||
layout = json.get("layout").asString ?: "default",
|
|
||||||
enableReleases = json.get("enableReleases")?.asBoolean ?: true,
|
|
||||||
enableSnapshots = json.get("enableSnapshots")?.asBoolean ?: true,
|
|
||||||
authentication = if (json.has("authentication") && json.get("authentication").isJsonObject)
|
|
||||||
context.deserialize<Authentication>(
|
|
||||||
json.getAsJsonObject("authentication"), Authentication::class.java
|
|
||||||
) else null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is JsonPrimitive -> {
|
|
||||||
MavenRepositoryConfig(url = URL(json.asString))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw JsonParseException("Unsupported Maven warehouse configuration type.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
29
scalabot-app/src/main/kotlin/util/TelegramBotAccounts.kt
Normal file
29
scalabot-app/src/main/kotlin/util/TelegramBotAccounts.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package net.lamgc.scalabot.util
|
||||||
|
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object TelegramBotAccounts {
|
||||||
|
|
||||||
|
private val botTokenPattern: Pattern = Pattern.compile("([1-9]\\d+):([A-Za-z\\d_-]{35,})")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 AbilityBot 的账户 Id.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 账户 Id 来自于 botToken 中, token 的格式为 "{AccountId}:{Secret}".
|
||||||
|
*
|
||||||
|
* 账户 Id 的真实性与 botToken 的有效性有关, 本方法并不会确保 botToken 的有效性, 一般情况下也无需考虑 Id 的有效性,
|
||||||
|
* 如果有需要, 可尝试通过调用 [org.telegram.telegrambots.meta.api.methods.GetMe] 来确保 botToken 的有效性.
|
||||||
|
*
|
||||||
|
* @param botToken 要获取账户 Id 的 botToken 字符串.
|
||||||
|
* @return 返回 AbilityBot 的账户 Id.
|
||||||
|
* @throws IllegalArgumentException 当 AbilityBot 的 botToken 格式错误时抛出该异常.
|
||||||
|
*/
|
||||||
|
fun getBotAccountId(botToken: String): Long {
|
||||||
|
val matcher: Matcher = botTokenPattern.matcher(botToken)
|
||||||
|
require(matcher.matches()) { "Invalid token format." }
|
||||||
|
return matcher.group(1).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,16 +10,14 @@ import java.io.FilenameFilter
|
|||||||
|
|
||||||
internal fun ByteArray.toHexString(): String = joinToString("") { it.toString(16) }
|
internal fun ByteArray.toHexString(): String = joinToString("") { it.toString(16) }
|
||||||
|
|
||||||
internal fun Artifact.equalsArtifact(that: Artifact): Boolean =
|
internal fun Artifact.equalsArtifact(that: Artifact, checkProperties: Boolean = false): Boolean =
|
||||||
this.groupId.equals(that.groupId) &&
|
this.groupId.equals(that.groupId) &&
|
||||||
this.artifactId.equals(that.artifactId) &&
|
this.artifactId.equals(that.artifactId) &&
|
||||||
this.version.equals(that.version) &&
|
this.version.equals(that.version) &&
|
||||||
this.baseVersion.equals(that.baseVersion) &&
|
|
||||||
this.isSnapshot == that.isSnapshot &&
|
|
||||||
this.classifier.equals(that.classifier) &&
|
this.classifier.equals(that.classifier) &&
|
||||||
this.extension.equals(that.extension) &&
|
this.extension.equals(that.extension) &&
|
||||||
(if (this.file == null) that.file == null else this.file.equals(that.file)) &&
|
(if (this.file == null) that.file == null else this.file.equals(that.file)) &&
|
||||||
this.properties.equals(that.properties)
|
(!checkProperties || this.properties.equals(that.properties))
|
||||||
|
|
||||||
internal fun File.deepListFiles(
|
internal fun File.deepListFiles(
|
||||||
addSelf: Boolean = false,
|
addSelf: Boolean = false,
|
||||||
@ -27,13 +25,13 @@ internal fun File.deepListFiles(
|
|||||||
fileFilter: FileFilter? = null,
|
fileFilter: FileFilter? = null,
|
||||||
filenameFilter: FilenameFilter? = null
|
filenameFilter: FilenameFilter? = null
|
||||||
): Array<File>? {
|
): Array<File>? {
|
||||||
val files = if (fileFilter != null) {
|
val files = (if (fileFilter != null) {
|
||||||
this.listFiles(fileFilter)
|
this.listFiles(fileFilter)
|
||||||
} else if (filenameFilter != null) {
|
} else if (filenameFilter != null) {
|
||||||
this.listFiles(filenameFilter)
|
this.listFiles(filenameFilter)
|
||||||
} else {
|
} else {
|
||||||
this.listFiles()
|
this.listFiles()
|
||||||
} ?: return null
|
}) ?: return null
|
||||||
|
|
||||||
val result = if (addSelf) mutableSetOf(this) else mutableSetOf()
|
val result = if (addSelf) mutableSetOf(this) else mutableSetOf()
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
@ -73,11 +71,10 @@ fun <T : AutoCloseable> T.registerShutdownHook(): T {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private val log = KotlinLogging.logger { }
|
|
||||||
|
|
||||||
private object UtilsInternal {
|
private object UtilsInternal {
|
||||||
|
|
||||||
val autoCloseableSet = mutableSetOf<AutoCloseable>()
|
val autoCloseableSet = mutableSetOf<AutoCloseable>()
|
||||||
|
private val log = KotlinLogging.logger(UtilsInternal::class.java.name)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))
|
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<included>
|
<included>
|
||||||
<define name="DATA_LOGS" class="net.lamgc.scalabot.LogDirectorySupplier"/>
|
<define name="DATA_LOGS" class="net.lamgc.scalabot.LogDirectorySupplier"/>
|
||||||
|
<define name="LOG_LEVEL" class="net.lamgc.scalabot.LogLevelSupplier"/>
|
||||||
|
<define name="NETWORK_LOG_LEVEL" class="net.lamgc.scalabot.NetworkVerboseLogSupplier"/>
|
||||||
|
|
||||||
<appender name="STD_OUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STD_OUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<encoder>
|
<encoder>
|
||||||
@ -24,7 +26,7 @@
|
|||||||
<appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<file>${DATA_LOGS}/latest.log</file>
|
<file>${DATA_LOGS}/latest.log</file>
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>data/logs/%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
<fileNamePattern>${DATA_LOGS}/%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||||
<maxHistory>30</maxHistory>
|
<maxHistory>30</maxHistory>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
<encoder>
|
<encoder>
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
<configuration scan="false" debug="false">
|
|
||||||
<include resource="base-logback.xml"/>
|
|
||||||
|
|
||||||
<logger name="org.apache.http" level="INFO"/>
|
|
||||||
<logger name="org.eclipse.aether.internal.impl.DefaultTransporterProvider" level="INFO"/>
|
|
||||||
<logger name="org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider" level="INFO"/>
|
|
||||||
<logger name="org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManager" level="INFO"/>
|
|
||||||
<logger name="org.telegram.telegrambots.facilities.proxysocketfactorys" level="INFO"/>
|
|
||||||
<logger name="org.eclipse.aether.internal.impl.DefaultUpdateCheckManager" level="INFO"/>
|
|
||||||
|
|
||||||
<root level="DEBUG">
|
|
||||||
<appender-ref ref="FILE_OUT"/>
|
|
||||||
<appender-ref ref="STD_ERR"/>
|
|
||||||
<appender-ref ref="STD_OUT"/>
|
|
||||||
</root>
|
|
||||||
</configuration>
|
|
@ -1,7 +1,14 @@
|
|||||||
<configuration scan="false" debug="false">
|
<configuration scan="false" debug="false">
|
||||||
<include resource="base-logback.xml"/>
|
<include resource="base-logback.xml"/>
|
||||||
|
|
||||||
<root level="INFO">
|
<logger name="org.apache.http" level="${NETWORK_LOG_LEVEL}"/>
|
||||||
|
<logger name="org.eclipse.aether.internal.impl.DefaultTransporterProvider" level="${NETWORK_LOG_LEVEL}"/>
|
||||||
|
<logger name="org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider" level="${NETWORK_LOG_LEVEL}"/>
|
||||||
|
<logger name="org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManager" level="${NETWORK_LOG_LEVEL}"/>
|
||||||
|
<logger name="org.telegram.telegrambots.facilities.proxysocketfactorys" level="${NETWORK_LOG_LEVEL}"/>
|
||||||
|
<logger name="org.eclipse.aether.internal.impl.DefaultUpdateCheckManager" level="${NETWORK_LOG_LEVEL}"/>
|
||||||
|
|
||||||
|
<root level="${LOG_LEVEL}">
|
||||||
<appender-ref ref="FILE_OUT"/>
|
<appender-ref ref="FILE_OUT"/>
|
||||||
<appender-ref ref="STD_ERR"/>
|
<appender-ref ref="STD_ERR"/>
|
||||||
<appender-ref ref="STD_OUT"/>
|
<appender-ref ref="STD_OUT"/>
|
||||||
|
@ -1,62 +1,60 @@
|
|||||||
package net.lamgc.scalabot
|
package net.lamgc.scalabot
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.github.stefanbirkner.systemlambda.SystemLambda
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.config.MavenRepositoryConfig
|
||||||
|
import net.lamgc.scalabot.config.ProxyConfig
|
||||||
|
import net.lamgc.scalabot.config.ProxyType
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.io.IOException
|
||||||
import kotlin.math.abs
|
import java.net.Proxy
|
||||||
import kotlin.test.Test
|
import java.net.URL
|
||||||
import kotlin.test.assertEquals
|
import java.nio.file.Files
|
||||||
import kotlin.test.assertNotNull
|
import java.nio.file.Path
|
||||||
import kotlin.test.assertTrue
|
import kotlin.io.path.deleteExisting
|
||||||
|
import kotlin.test.*
|
||||||
internal class BotAccountTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deserializerTest() {
|
|
||||||
val accountId = abs(Random().nextInt()).toLong()
|
|
||||||
val creatorId = abs(Random().nextInt()).toLong()
|
|
||||||
val botAccount = Gson().fromJson(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"name": "TestBot",
|
|
||||||
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
|
||||||
"creatorId": $creatorId
|
|
||||||
}
|
|
||||||
""".trimIndent(), BotAccount::class.java
|
|
||||||
)
|
|
||||||
assertEquals("TestBot", botAccount.name)
|
|
||||||
assertEquals("${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", botAccount.token)
|
|
||||||
assertEquals(accountId, botAccount.id, "Botaccount ID does not match expectations.")
|
|
||||||
assertEquals(creatorId, botAccount.creatorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class AppPathsTest {
|
internal class AppPathsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Data root path priority`() {
|
fun `Consistency check`() {
|
||||||
System.setProperty("bot.path.data", "A")
|
for (path in AppPaths.entries) {
|
||||||
|
|
||||||
assertEquals("A", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
|
|
||||||
System.getProperties().remove("bot.path.data")
|
|
||||||
if (System.getenv("BOT_DATA_PATH") != null) {
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
System.getenv("BOT_DATA_PATH"), AppPaths.DATA_ROOT.file.path,
|
File(path.path).canonicalPath,
|
||||||
"`DATA_ROOT`没有返回 env 的值."
|
path.file.canonicalPath,
|
||||||
|
"路径 File 与 Path 不一致: ${path.name}"
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Data root path priority`() {
|
||||||
|
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, "fromSystemProperties")
|
||||||
|
|
||||||
|
assertEquals("fromSystemProperties", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
|
||||||
|
System.getProperties().remove(AppPaths.PathConst.PROP_DATA_PATH)
|
||||||
|
|
||||||
|
val expectEnvValue = "fromEnvironmentVariable"
|
||||||
|
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, expectEnvValue).execute {
|
||||||
|
assertEquals(
|
||||||
|
expectEnvValue, AppPaths.DATA_ROOT.file.path,
|
||||||
|
"`DATA_ROOT`没有优先返回 env 的值."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, null).execute {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
System.getProperty("user.dir"), AppPaths.DATA_ROOT.file.path,
|
System.getProperty("user.dir"), AppPaths.DATA_ROOT.file.path,
|
||||||
"`DATA_ROOT`没有返回 `user.dir` 的值."
|
"`DATA_ROOT`没有返回 System.properties `user.dir` 的值."
|
||||||
)
|
)
|
||||||
val userDir = System.getProperty("user.dir")
|
val userDir = System.getProperty("user.dir")
|
||||||
System.getProperties().remove("user.dir")
|
System.getProperties().remove("user.dir")
|
||||||
assertEquals(".", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有返回 `.`(当前目录).")
|
assertEquals(".", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有返回替补值 `.`(当前目录).")
|
||||||
System.setProperty("user.dir", userDir)
|
System.setProperty("user.dir", userDir)
|
||||||
assertNotNull(System.getProperty("user.dir"), "环境还原失败!")
|
assertNotNull(System.getProperty("user.dir"), "环境还原失败!")
|
||||||
}
|
}
|
||||||
@ -115,8 +113,304 @@ internal class AppPathsTest {
|
|||||||
verify(exactly = 0) { mkdirs() }
|
verify(exactly = 0) { mkdirs() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mockk<File> {
|
||||||
|
every { exists() }.returns(false)
|
||||||
|
every { canonicalPath }.answers { alreadyExistsFile.canonicalPath }
|
||||||
|
every { createNewFile() }.answers { false }
|
||||||
|
every { mkdirs() }.answers { false }
|
||||||
|
every { mkdir() }.answers { false }
|
||||||
|
}.apply {
|
||||||
|
mockk<AppPaths> {
|
||||||
|
every { file }.returns(this@apply)
|
||||||
|
every { path }.returns(this@apply.canonicalPath)
|
||||||
|
every { initial() }.answers {
|
||||||
|
defaultInitializerMethod.invoke(null, this@mockk)
|
||||||
|
}
|
||||||
|
}.initial()
|
||||||
|
verify(exactly = 1) { createNewFile() }
|
||||||
|
verify(exactly = 0) { mkdir() }
|
||||||
|
verify(exactly = 0) { mkdirs() }
|
||||||
|
}
|
||||||
|
|
||||||
defaultInitializerMethod.isAccessible = false
|
defaultInitializerMethod.isAccessible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Test
|
||||||
|
fun `loadBotConfig test`(@TempDir testDir: File) {
|
||||||
|
assertNull(loadBotConfigJson(File("/NOT_EXISTS_FILE")), "加载 BotConfigs 失败时应该返回 null.")
|
||||||
|
|
||||||
|
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, testDir.canonicalPath).execute {
|
||||||
|
assertNull(loadBotConfigJson(), "加载 BotConfigs 失败时应该返回 null.")
|
||||||
|
|
||||||
|
File(testDir, "bot.json").apply {
|
||||||
|
//language=JSON5
|
||||||
|
writeText(
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"account": {
|
||||||
|
"name": "TestBot",
|
||||||
|
"token": "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||||
|
"creatorId": 123456789
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080,
|
||||||
|
"type": "HTTP"
|
||||||
|
},
|
||||||
|
"disableBuiltInAbility": false,
|
||||||
|
"autoUpdateCommandList": true,
|
||||||
|
"extensions": [
|
||||||
|
"org.example.test:test-extension:1.0.0"
|
||||||
|
],
|
||||||
|
"baseApiUrl": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val botConfigJsons = loadBotConfigJson()
|
||||||
|
assertNotNull(botConfigJsons)
|
||||||
|
assertEquals(1, botConfigJsons.size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadAppConfig test`(@TempDir testDir: File) {
|
||||||
|
assertThrows<IOException>("加载失败时应该抛出 IOException.") {
|
||||||
|
loadAppConfig(File("/NOT_EXISTS_FILE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLambda.withEnvironmentVariable(AppPaths.PathConst.ENV_DATA_PATH, testDir.canonicalPath).execute {
|
||||||
|
assertThrows<IOException>("加载失败时应该抛出 IOException.") {
|
||||||
|
loadAppConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
File(testDir, "config.json").apply {
|
||||||
|
//language=JSON5
|
||||||
|
writeText(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"proxy": {
|
||||||
|
"type": "HTTP",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"enable": true,
|
||||||
|
"port": 8800,
|
||||||
|
"bindAddress": "127.0.0.1",
|
||||||
|
"authenticator": {
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mavenRepositories": [
|
||||||
|
{
|
||||||
|
"url": "https://repository.maven.apache.org/maven2/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mavenLocalRepository": "file:///tmp/maven-local-repository"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val appConfigs = loadAppConfig()
|
||||||
|
assertNotNull(appConfigs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ProxyType_toTelegramBotsType test`() {
|
||||||
|
val expectTypeMapping = mapOf(
|
||||||
|
ProxyType.NO_PROXY to null,
|
||||||
|
ProxyType.SOCKS5 to Proxy.Type.SOCKS,
|
||||||
|
ProxyType.SOCKS4 to Proxy.Type.SOCKS,
|
||||||
|
ProxyType.HTTP to Proxy.Type.HTTP,
|
||||||
|
ProxyType.HTTPS to Proxy.Type.HTTP
|
||||||
|
)
|
||||||
|
|
||||||
|
for (proxyType in ProxyType.entries) {
|
||||||
|
assertEquals(
|
||||||
|
expectTypeMapping[proxyType],
|
||||||
|
proxyType.toJavaProxyType(),
|
||||||
|
"ProxyType 转换失败."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ProxyConfig_toAetherProxy test`() {
|
||||||
|
val host = "proxy.example.org"
|
||||||
|
val port = 1080
|
||||||
|
|
||||||
|
val expectNotNullProxyType = setOf(
|
||||||
|
ProxyType.HTTP,
|
||||||
|
ProxyType.HTTPS
|
||||||
|
)
|
||||||
|
for (proxyType in ProxyType.entries) {
|
||||||
|
val proxyConfig = ProxyConfig(proxyType, host, port)
|
||||||
|
val aetherProxy = proxyConfig.toAetherProxy()
|
||||||
|
if (expectNotNullProxyType.contains(proxyType)) {
|
||||||
|
assertNotNull(aetherProxy, "支持的代理类型应该不为 null.")
|
||||||
|
assertEquals(host, aetherProxy.host)
|
||||||
|
assertEquals(port, aetherProxy.port)
|
||||||
|
} else {
|
||||||
|
assertNull(aetherProxy, "不支持的代理类型应该返回 null.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MavenRepositoryConfig_toRemoteRepository test`() {
|
||||||
|
val defaultMavenRepositoryConfig = MavenRepositoryConfig(
|
||||||
|
url = URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL),
|
||||||
|
enableReleases = true,
|
||||||
|
enableSnapshots = false
|
||||||
|
)
|
||||||
|
val remoteRepositoryWithoutId = defaultMavenRepositoryConfig.toRemoteRepository(
|
||||||
|
ProxyConfig(ProxyType.NO_PROXY, "", 0)
|
||||||
|
)
|
||||||
|
assertEquals(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL, remoteRepositoryWithoutId.url.toString())
|
||||||
|
assertNotNull(remoteRepositoryWithoutId.id)
|
||||||
|
assertTrue(remoteRepositoryWithoutId.getPolicy(false).isEnabled)
|
||||||
|
assertFalse(remoteRepositoryWithoutId.getPolicy(true).isEnabled)
|
||||||
|
|
||||||
|
val remoteRepositoryWithId = defaultMavenRepositoryConfig.copy(id = "test-repo").toRemoteRepository(
|
||||||
|
ProxyConfig(ProxyType.HTTP, "127.0.0.1", 1080)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("test-repo", remoteRepositoryWithId.id)
|
||||||
|
assertEquals(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL, remoteRepositoryWithId.url.toString())
|
||||||
|
assertEquals("http", remoteRepositoryWithId.proxy.type)
|
||||||
|
assertEquals("127.0.0.1", remoteRepositoryWithId.proxy.host)
|
||||||
|
assertEquals(1080, remoteRepositoryWithId.proxy.port)
|
||||||
|
assertEquals(remoteRepositoryWithId.id, remoteRepositoryWithId.id)
|
||||||
|
|
||||||
|
val remoteRepositoryWithProxy = defaultMavenRepositoryConfig.copy(
|
||||||
|
id = "test-repo",
|
||||||
|
proxy = ProxyConfig(ProxyType.HTTP, "example.org", 1080).toAetherProxy()
|
||||||
|
).toRemoteRepository(ProxyConfig(ProxyType.HTTP, "localhost", 8080))
|
||||||
|
assertEquals("http", remoteRepositoryWithProxy.proxy.type)
|
||||||
|
assertEquals("example.org", remoteRepositoryWithProxy.proxy.host, "未优先使用 MavenRepositoryConfig 中的 proxy 属性.")
|
||||||
|
assertEquals(1080, remoteRepositoryWithProxy.proxy.port, "未优先使用 MavenRepositoryConfig 中的 proxy 属性.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `checkRepositoryLayout test`() {
|
||||||
|
val noProxyConfig = ProxyConfig(ProxyType.NO_PROXY, "", 0)
|
||||||
|
assertEquals(
|
||||||
|
"default", MavenRepositoryConfig(url = URL("https://repo.example.org"))
|
||||||
|
.toRemoteRepository(noProxyConfig).contentType
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"legacy", MavenRepositoryConfig(url = URL("https://repo.example.org"), layout = "LEgaCY")
|
||||||
|
.toRemoteRepository(noProxyConfig).contentType
|
||||||
|
)
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
MavenRepositoryConfig(
|
||||||
|
url = URL("https://repo.example.org"),
|
||||||
|
layout = "NOT_EXISTS_LAYOUT"
|
||||||
|
).toRemoteRepository(noProxyConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initialFiles test`(@TempDir testDir: Path) {
|
||||||
|
// 这么做是为了让日志文件创建在其他地方, 由于日志文件在运行时会持续占用, 在 windows 中文件会被锁定,
|
||||||
|
// 导致测试框架无法正常清除测试所使用的临时文件夹.
|
||||||
|
val logsDir = Files.createTempDirectory("ammmmmm-logs-")
|
||||||
|
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, logsDir.toString())
|
||||||
|
assertEquals(logsDir.toString(), AppPaths.DATA_ROOT.path, "日志目录设定失败.")
|
||||||
|
KotlinLogging.logger("TEST").error { "日志占用.(无需理会), 日志目录: $logsDir" }
|
||||||
|
AppPaths.DATA_LOGS.file.listFiles { _, name -> name.endsWith(".log") }?.forEach {
|
||||||
|
it.deleteOnExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fullInitializeDir = Files.createTempDirectory(testDir, "fullInitialize")
|
||||||
|
fullInitializeDir.deleteExisting()
|
||||||
|
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, fullInitializeDir.toString())
|
||||||
|
assertEquals(fullInitializeDir.toString(), AppPaths.DATA_ROOT.path, "测试路径设定失败.")
|
||||||
|
|
||||||
|
assertTrue(initialFiles(), "方法未能提醒用户编辑初始配置文件.")
|
||||||
|
|
||||||
|
for (path in AppPaths.entries) {
|
||||||
|
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||||
|
if (path.file.isFile) {
|
||||||
|
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||||
|
}
|
||||||
|
path.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(initialFiles(), "方法试图在配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||||
|
|
||||||
|
for (path in AppPaths.entries) {
|
||||||
|
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||||
|
if (path.file.isFile) {
|
||||||
|
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||||
|
}
|
||||||
|
path.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(AppPaths.CONFIG_APPLICATION.file.delete(), "config.json 删除失败.")
|
||||||
|
assertFalse(initialFiles(), "方法试图在部分配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||||
|
|
||||||
|
for (path in AppPaths.entries) {
|
||||||
|
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||||
|
if (path.file.isFile) {
|
||||||
|
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||||
|
}
|
||||||
|
path.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(AppPaths.CONFIG_BOT.file.delete(), "bot.json 删除失败.")
|
||||||
|
assertFalse(initialFiles(), "方法试图在部分配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||||
|
|
||||||
|
for (path in AppPaths.entries) {
|
||||||
|
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||||
|
if (path.file.isFile) {
|
||||||
|
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||||
|
}
|
||||||
|
path.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(AppPaths.CONFIG_APPLICATION.file.delete(), "config.json 删除失败.")
|
||||||
|
assertTrue(AppPaths.CONFIG_BOT.file.delete(), "bot.json 删除失败.")
|
||||||
|
assertTrue(
|
||||||
|
initialFiles(),
|
||||||
|
"在主要配置文件(config.json 和 bot.json)不存在的情况下初始化文件后, 方法未能提醒用户编辑初始配置文件."
|
||||||
|
)
|
||||||
|
|
||||||
|
for (path in AppPaths.entries) {
|
||||||
|
assertTrue(path.file.exists(), "文件未初始化成功: ${path.path}")
|
||||||
|
if (path.file.isFile) {
|
||||||
|
assertNotEquals(0, path.file.length(), "文件未初始化成功(大小为 0): ${path.path}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPaths.CONFIG_APPLICATION.file.writeText("Test-APPLICATION")
|
||||||
|
AppPaths.CONFIG_BOT.file.writeText("Test-BOT")
|
||||||
|
assertFalse(initialFiles(), "方法试图在部分配置已初始化的情况下提醒用户编辑初始配置文件.")
|
||||||
|
assertEquals(
|
||||||
|
"Test-APPLICATION", AppPaths.CONFIG_APPLICATION.file.readText(),
|
||||||
|
"config.json 被覆盖. initialized 并未阻止重复初始化."
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Test-BOT", AppPaths.CONFIG_BOT.file.readText(),
|
||||||
|
"bot.json 被覆盖. initialized 并未阻止重复初始化."
|
||||||
|
)
|
||||||
|
|
||||||
|
System.getProperties().remove(AppPaths.PathConst.PROP_DATA_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AppPaths.reset() {
|
||||||
|
val method = AppPaths::class.java.getDeclaredMethod("reset")
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(this)
|
||||||
|
method.isAccessible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
@file:Suppress("PackageDirectoryMismatch")
|
|
||||||
|
|
||||||
package net.lamgc.scalabot.util
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import org.eclipse.aether.artifact.DefaultArtifact
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
internal class ArtifactSerializerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun badJsonType() {
|
|
||||||
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonObject(), null, null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun serialize() {
|
|
||||||
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
|
||||||
val expectArtifact = DefaultArtifact(gav)
|
|
||||||
val actualArtifact = DefaultArtifact(ArtifactSerializer.serialize(expectArtifact, null, null).asString)
|
|
||||||
assertEquals(expectArtifact, actualArtifact)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deserialize() {
|
|
||||||
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
|
||||||
val expectArtifact = DefaultArtifact(gav)
|
|
||||||
val actualArtifact = ArtifactSerializer.deserialize(JsonPrimitive(gav), null, null)
|
|
||||||
assertEquals(expectArtifact, actualArtifact)
|
|
||||||
}
|
|
||||||
}
|
|
40
scalabot-app/src/test/kotlin/util/StdOutFilterTest.kt
Normal file
40
scalabot-app/src/test/kotlin/util/StdOutFilterTest.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package net.lamgc.scalabot.util
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level
|
||||||
|
import ch.qos.logback.classic.spi.LoggingEvent
|
||||||
|
import ch.qos.logback.core.spi.FilterReply
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class StdOutFilterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filterTest() {
|
||||||
|
val filter = StdOutFilter()
|
||||||
|
|
||||||
|
for (level in listOf(
|
||||||
|
Level.ALL,
|
||||||
|
Level.TRACE,
|
||||||
|
Level.DEBUG,
|
||||||
|
Level.INFO
|
||||||
|
)) {
|
||||||
|
val loggingEvent = mockk<LoggingEvent> {
|
||||||
|
every { this@mockk.level }.returns(level)
|
||||||
|
}
|
||||||
|
assertEquals(FilterReply.ACCEPT, filter.decide(loggingEvent))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (level in listOf(
|
||||||
|
Level.WARN,
|
||||||
|
Level.ERROR
|
||||||
|
)) {
|
||||||
|
val loggingEvent = mockk<LoggingEvent> {
|
||||||
|
every { this@mockk.level }.returns(level)
|
||||||
|
}
|
||||||
|
assertEquals(FilterReply.DENY, filter.decide(loggingEvent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
scalabot-app/src/test/kotlin/util/TelegramBotAccountsTest.kt
Normal file
27
scalabot-app/src/test/kotlin/util/TelegramBotAccountsTest.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import net.lamgc.scalabot.util.TelegramBotAccounts
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class TelegramBotAccountsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getBotAccountIdTest() {
|
||||||
|
val expectToken = "1234567890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo"
|
||||||
|
val actual: Long = TelegramBotAccounts.getBotAccountId(expectToken)
|
||||||
|
assertEquals(1234567890, actual)
|
||||||
|
|
||||||
|
val badTokenA = "12c34d56a7890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo"
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException::class.java
|
||||||
|
) { TelegramBotAccounts.getBotAccountId(badTokenA) }
|
||||||
|
|
||||||
|
val badTokenB = "12c34d56a7890AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo"
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException::class.java
|
||||||
|
) { TelegramBotAccounts.getBotAccountId(badTokenB) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,9 +1,6 @@
|
|||||||
package net.lamgc.scalabot.util
|
package net.lamgc.scalabot.util
|
||||||
|
|
||||||
import io.mockk.every
|
import io.mockk.*
|
||||||
import io.mockk.justRun
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.verify
|
|
||||||
import net.lamgc.scalabot.ExtensionPackageFinder
|
import net.lamgc.scalabot.ExtensionPackageFinder
|
||||||
import net.lamgc.scalabot.FinderPriority
|
import net.lamgc.scalabot.FinderPriority
|
||||||
import net.lamgc.scalabot.FinderRules
|
import net.lamgc.scalabot.FinderRules
|
||||||
@ -13,6 +10,8 @@ import org.eclipse.aether.artifact.DefaultArtifact
|
|||||||
import org.junit.jupiter.api.assertDoesNotThrow
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileFilter
|
||||||
|
import java.io.FilenameFilter
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
@ -116,4 +115,203 @@ internal class UtilsKtTest {
|
|||||||
|
|
||||||
resourceSet.clear()
|
resourceSet.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Artifact equals`() {
|
||||||
|
val artifact = DefaultArtifact("org.example:artifact:jar:0.0.1")
|
||||||
|
assertFalse(artifact.isSnapshot, "Release artifact is snapshot.")
|
||||||
|
assertTrue(artifact.equalsArtifact(artifact))
|
||||||
|
assertTrue(artifact.setFile(File(".")).equalsArtifact(artifact.setFile(File("."))))
|
||||||
|
val snapshotArtifact = DefaultArtifact("org.example:artifact:jar:0.0.1-SNAPSHOT")
|
||||||
|
val snapshotTimestampArtifact = DefaultArtifact("org.example:artifact:jar:0.0.1-20220605.130047-1")
|
||||||
|
assertTrue(snapshotArtifact.isSnapshot, "SnapshotArtifact not snapshot.")
|
||||||
|
assertNotEquals(artifact.isSnapshot, snapshotArtifact.isSnapshot)
|
||||||
|
assertNotEquals(artifact.baseVersion, snapshotArtifact.baseVersion)
|
||||||
|
assertFalse(artifact.equalsArtifact(snapshotArtifact))
|
||||||
|
assertFalse(snapshotArtifact.equalsArtifact(snapshotTimestampArtifact))
|
||||||
|
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:0.0.2")))
|
||||||
|
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example.test:artifact:0.0.1")))
|
||||||
|
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact-a:0.0.1")))
|
||||||
|
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:war:0.0.1")))
|
||||||
|
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:war:javadoc:0.0.1")))
|
||||||
|
assertFalse(artifact.equalsArtifact(DefaultArtifact("org.example:artifact:rar:source:0.0.1")))
|
||||||
|
|
||||||
|
assertFalse(
|
||||||
|
artifact.equalsArtifact(
|
||||||
|
DefaultArtifact("org.example:artifact:jar:0.0.1")
|
||||||
|
.setFile(File("./xxx01.jar"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val artifactWithExtension = DefaultArtifact("org.example:artifact:jar:0.0.1")
|
||||||
|
assertFalse(artifactWithExtension.equalsArtifact(DefaultArtifact("org.example:artifact:war:0.0.1")))
|
||||||
|
|
||||||
|
assertTrue(artifact.equalsArtifact(artifact.setProperties(mapOf(Pair("a", "b"))), checkProperties = false))
|
||||||
|
assertFalse(artifact.equalsArtifact(artifact.setProperties(mapOf(Pair("a", "b"))), checkProperties = true))
|
||||||
|
assertTrue(
|
||||||
|
artifact.setProperties(mapOf(Pair("a", "b")))
|
||||||
|
.equalsArtifact(artifact.setProperties(mapOf(Pair("a", "b"))), checkProperties = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deepListFile Test - Basics`() {
|
||||||
|
assertNull(mockk<File> {
|
||||||
|
every { listFiles() } returns null
|
||||||
|
}.deepListFiles())
|
||||||
|
assertNull(mockk<File> {
|
||||||
|
every { listFiles(ofType(FileFilter::class)) } returns null
|
||||||
|
}.deepListFiles(fileFilter = { true }))
|
||||||
|
assertNull(mockk<File> {
|
||||||
|
every { listFiles(ofType(FilenameFilter::class)) } returns null
|
||||||
|
}.deepListFiles(filenameFilter = { _, _ -> true }))
|
||||||
|
|
||||||
|
|
||||||
|
val listFileMock = mockk<File> {
|
||||||
|
every { listFiles() } returns arrayOf()
|
||||||
|
every { listFiles(ofType(FileFilter::class)) } returns arrayOf()
|
||||||
|
every { listFiles(ofType(FilenameFilter::class)) } returns arrayOf()
|
||||||
|
}
|
||||||
|
assertNotNull(listFileMock.deepListFiles())
|
||||||
|
verify(exactly = 1) { listFileMock.listFiles() }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||||
|
clearMocks(listFileMock, answers = false)
|
||||||
|
|
||||||
|
assertNotNull(listFileMock.deepListFiles(filenameFilter = { _, _ -> true }))
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles() }
|
||||||
|
verify(exactly = 1) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||||
|
clearMocks(listFileMock, answers = false)
|
||||||
|
|
||||||
|
assertNotNull(listFileMock.deepListFiles(fileFilter = { true }))
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles() }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||||
|
verify(exactly = 1) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||||
|
clearMocks(listFileMock, answers = false)
|
||||||
|
|
||||||
|
assertNotNull(listFileMock.deepListFiles(fileFilter = { true }, filenameFilter = { _, _ -> true }))
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles() }
|
||||||
|
verify(exactly = 1) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||||
|
clearMocks(listFileMock, answers = false)
|
||||||
|
|
||||||
|
val addSelfResult = listFileMock.deepListFiles(addSelf = true)
|
||||||
|
assertNotNull(addSelfResult)
|
||||||
|
assertEquals(1, addSelfResult.size)
|
||||||
|
assertTrue(addSelfResult.contains(listFileMock))
|
||||||
|
verify(exactly = 1) { listFileMock.listFiles() }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FilenameFilter::class)) }
|
||||||
|
verify(exactly = 0) { listFileMock.listFiles(ofType(FileFilter::class)) }
|
||||||
|
|
||||||
|
val addSelfWithoutDirMock = createDirectory(
|
||||||
|
"root", arrayOf(
|
||||||
|
createDirectory(
|
||||||
|
"dir01", arrayOf(
|
||||||
|
createFile("test01")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
createDirectory(
|
||||||
|
"dir02", arrayOf(
|
||||||
|
createFile("test02")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
createDirectory(
|
||||||
|
"dir03", arrayOf(
|
||||||
|
createFile("test03")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val addSelfWithoutDirResult = addSelfWithoutDirMock.deepListFiles(addSelf = true, onlyFile = true)
|
||||||
|
assertNotNull(addSelfWithoutDirResult)
|
||||||
|
assertFalse(addSelfWithoutDirResult.isEmpty())
|
||||||
|
assertEquals(1, addSelfWithoutDirResult.filter { it.isDirectory }.size)
|
||||||
|
assertEquals(addSelfWithoutDirMock, addSelfWithoutDirResult.find { it.isDirectory })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deepListFile Test - Complex`() {
|
||||||
|
val mock = createDirectory(
|
||||||
|
"root", arrayOf(
|
||||||
|
createFile("test"),
|
||||||
|
createFile("test02"),
|
||||||
|
createDirectory("dir01"),
|
||||||
|
createDirectory("dir02")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val withDirResult = mock.deepListFiles(onlyFile = false)
|
||||||
|
assertNotNull(withDirResult)
|
||||||
|
assertEquals(4, withDirResult.size)
|
||||||
|
assertEquals(2, withDirResult.filter { it.isFile }.size)
|
||||||
|
assertEquals(2, withDirResult.filter { it.isDirectory }.size)
|
||||||
|
|
||||||
|
val withoutDirResult = mock.deepListFiles(onlyFile = true)
|
||||||
|
assertNotNull(withoutDirResult)
|
||||||
|
assertEquals(2, withoutDirResult.filter { it.isFile }.size)
|
||||||
|
assertNull(withoutDirResult.find { it.isDirectory })
|
||||||
|
|
||||||
|
val subDirFailedMock = createDirectory(
|
||||||
|
"root", arrayOf(
|
||||||
|
mockk(name = "dir::cannotReadableDirectory") {
|
||||||
|
every { isFile } returns false
|
||||||
|
every { isDirectory } returns true
|
||||||
|
every { name } returns "cannotReadableDirectory"
|
||||||
|
every { listFiles() } returns null
|
||||||
|
every { listFiles(ofType(FileFilter::class)) } returns null
|
||||||
|
every { listFiles(ofType(FilenameFilter::class)) } returns null
|
||||||
|
},
|
||||||
|
createDirectory(
|
||||||
|
"dir2", arrayOf(
|
||||||
|
createFile("test")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val subDirFailedWithDirResult = subDirFailedMock.deepListFiles(onlyFile = false)
|
||||||
|
assertNotNull(subDirFailedWithDirResult)
|
||||||
|
assertEquals(3, subDirFailedWithDirResult.size)
|
||||||
|
assertNotNull(subDirFailedWithDirResult.find { it.isDirectory && it.name == "cannotReadableDirectory" })
|
||||||
|
assertNotNull(subDirFailedWithDirResult.find { it.isDirectory && it.name == "dir2" })
|
||||||
|
assertNotNull(subDirFailedWithDirResult.find { it.isFile && it.name == "test" })
|
||||||
|
|
||||||
|
val subDirFailedWithoutDirResult = subDirFailedMock.deepListFiles(onlyFile = true)
|
||||||
|
assertNotNull(subDirFailedWithoutDirResult)
|
||||||
|
assertEquals(1, subDirFailedWithoutDirResult.size)
|
||||||
|
assertEquals(0, subDirFailedWithoutDirResult.filter { it.isDirectory }.size)
|
||||||
|
assertNotNull(subDirFailedWithoutDirResult.find { it.isFile && it.name == "test" })
|
||||||
|
assertNull(subDirFailedWithoutDirResult.find { it.isDirectory && it.name == "cannotReadableDirectory" })
|
||||||
|
assertNull(subDirFailedWithoutDirResult.find { it.isDirectory && it.name == "dir2" })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFile(path: String): File {
|
||||||
|
val file = File(path)
|
||||||
|
return mockk(name = "file::$path") {
|
||||||
|
every { isFile } returns true
|
||||||
|
every { isDirectory } returns false
|
||||||
|
every { name } returns file.name
|
||||||
|
every { listFiles() } returns null
|
||||||
|
every { listFiles(ofType(FileFilter::class)) } returns null
|
||||||
|
every { listFiles(ofType(FilenameFilter::class)) } returns null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDirectory(path: String, subFiles: Array<File> = arrayOf()): File {
|
||||||
|
val file = File(path)
|
||||||
|
return mockk(name = "dir::$path") {
|
||||||
|
every { isFile } returns false
|
||||||
|
every { isDirectory } returns true
|
||||||
|
every { name } returns file.name
|
||||||
|
every { listFiles() } returns subFiles
|
||||||
|
every { listFiles(ofType(FileFilter::class)) } answers {
|
||||||
|
subFiles.filter { (firstArg() as FileFilter).accept(it) }.toTypedArray()
|
||||||
|
}
|
||||||
|
every { listFiles(ofType(FilenameFilter::class)) } answers {
|
||||||
|
subFiles.filter { (firstArg() as FilenameFilter).accept(file.parentFile, file.name) }.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -2,17 +2,19 @@ plugins {
|
|||||||
java
|
java
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":scalabot-extension"))
|
compileOnly(project(":scalabot-extension"))
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
|
||||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.getByName<Test>("test") {
|
tasks.getByName<Test>("test") {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<Javadoc> {
|
||||||
|
options {
|
||||||
|
encoding = "UTF-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package net.lamgc.scalabot.simple;
|
package net.lamgc.scalabot.simple;
|
||||||
|
|
||||||
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
|
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
|
||||||
import org.telegram.abilitybots.api.objects.*;
|
import org.telegram.telegrambots.abilitybots.api.objects.*;
|
||||||
import org.telegram.abilitybots.api.util.AbilityExtension;
|
import org.telegram.telegrambots.abilitybots.api.util.AbilityExtension;
|
||||||
|
|
||||||
public class SayHelloExtension implements AbilityExtension {
|
public class SayHelloExtension implements AbilityExtension {
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ public class SayHelloExtension implements AbilityExtension {
|
|||||||
String msg = "Hello! " + ctx.user().getUserName() +
|
String msg = "Hello! " + ctx.user().getUserName() +
|
||||||
" ( " + ctx.user().getId() + " ) [ " + ctx.user().getLanguageCode() + " ]" + "\n" +
|
" ( " + ctx.user().getId() + " ) [ " + ctx.user().getLanguageCode() + " ]" + "\n" +
|
||||||
"Current Chat ID: " + ctx.chatId();
|
"Current Chat ID: " + ctx.chatId();
|
||||||
ctx.bot().silent().send(msg, ctx.chatId());
|
ctx.bot().getSilent().send(msg, ctx.chatId());
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -36,13 +36,13 @@ public class SayHelloExtension implements AbilityExtension {
|
|||||||
* 更具特色的 `Say hello`.
|
* 更具特色的 `Say hello`.
|
||||||
*/
|
*/
|
||||||
public Ability test() {
|
public Ability test() {
|
||||||
ReplyFlow botHello = ReplyFlow.builder(bot.db())
|
ReplyFlow botHello = ReplyFlow.builder(bot.getDb())
|
||||||
.enableStats("say_hello")
|
.enableStats("say_hello")
|
||||||
.action((bot, upd) -> bot.silent().send("What is u name?", upd.getMessage().getChatId()))
|
.action((bot, upd) -> bot.getSilent().send("What is u name?", upd.getMessage().getChatId()))
|
||||||
.onlyIf(update -> update.hasMessage()
|
.onlyIf(update -> update.hasMessage()
|
||||||
&& update.getMessage().hasText()
|
&& update.getMessage().hasText()
|
||||||
&& "hello".equalsIgnoreCase(update.getMessage().getText()))
|
&& "hello".equalsIgnoreCase(update.getMessage().getText()))
|
||||||
.next(Reply.of((bot, upd) -> bot.silent()
|
.next(Reply.of((bot, upd) -> bot.getSilent()
|
||||||
.send("OK! You name is " + upd.getMessage().getText().substring("my name is ".length()), upd.getMessage().getChatId()),
|
.send("OK! You name is " + upd.getMessage().getText().substring("my name is ".length()), upd.getMessage().getChatId()),
|
||||||
upd -> upd.hasMessage()
|
upd -> upd.hasMessage()
|
||||||
&& upd.getMessage().hasText()
|
&& upd.getMessage().hasText()
|
||||||
@ -55,7 +55,7 @@ public class SayHelloExtension implements AbilityExtension {
|
|||||||
.locality(Locality.ALL)
|
.locality(Locality.ALL)
|
||||||
.privacy(Privacy.PUBLIC)
|
.privacy(Privacy.PUBLIC)
|
||||||
.enableStats()
|
.enableStats()
|
||||||
.action(ctx -> ctx.bot().silent().send("Hello!", ctx.chatId()))
|
.action(ctx -> ctx.bot().getSilent().send("Hello!", ctx.chatId()))
|
||||||
.reply(botHello)
|
.reply(botHello)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
package net.lamgc.scalabot.simple;
|
package net.lamgc.scalabot.simple;
|
||||||
|
|
||||||
|
import net.lamgc.scalabot.extension.BotExtensionCreateOptions;
|
||||||
import net.lamgc.scalabot.extension.BotExtensionFactory;
|
import net.lamgc.scalabot.extension.BotExtensionFactory;
|
||||||
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
|
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
|
||||||
import org.telegram.abilitybots.api.util.AbilityExtension;
|
import org.telegram.telegrambots.abilitybots.api.util.AbilityExtension;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
public class SimpleExtensionFactory implements BotExtensionFactory {
|
public class SimpleExtensionFactory implements BotExtensionFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder) {
|
public AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder, BotExtensionCreateOptions options) {
|
||||||
return new SayHelloExtension(bot);
|
return new SayHelloExtension(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.6.10"
|
`java-library`
|
||||||
java
|
jacoco
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
signing
|
signing
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api("org.telegram:telegrambots-abilities:6.0.1")
|
implementation("commons-codec:commons-codec:1.16.1")
|
||||||
api("org.slf4j:slf4j-api:1.7.36")
|
api("org.telegram:telegrambots-abilities:8.0.0")
|
||||||
|
api(project(":scalabot-meta"))
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
|
||||||
testImplementation("org.mockito:mockito-core:4.4.0")
|
testImplementation("org.mockito:mockito-core:5.11.0")
|
||||||
|
testImplementation("org.telegram:telegrambots-client:8.0.0")
|
||||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,33 +25,30 @@ tasks.withType<Javadoc> {
|
|||||||
java {
|
java {
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
finalizedBy(tasks.jacocoTestReport)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.jacocoTestReport {
|
||||||
kotlinOptions.jvmTarget = "11"
|
dependsOn(tasks.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||||
|
isPreserveFileTimestamps = false
|
||||||
|
isReproducibleFileOrder = true
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
if (project.version.toString().endsWith("-SNAPSHOT")) {
|
maven("https://git.lamgc.me/api/packages/LamGC/maven") {
|
||||||
maven("https://repo.lamgc.moe/repository/maven-snapshots/") {
|
credentials {
|
||||||
credentials {
|
username = project.properties["repo.credentials.self-git.username"].toString()
|
||||||
username = project.properties["repo.credentials.private.username"].toString()
|
password = project.properties["repo.credentials.self-git.password"].toString()
|
||||||
password = project.properties["repo.credentials.private.password"].toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
maven("https://repo.lamgc.moe/repository/maven-releases/") {
|
|
||||||
credentials {
|
|
||||||
username = project.properties["repo.credentials.private.username"].toString()
|
|
||||||
password = project.properties["repo.credentials.private.password"].toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package net.lamgc.scalabot.extension;
|
||||||
|
|
||||||
|
import net.lamgc.scalabot.config.ProxyConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BotExtension 创建参数.
|
||||||
|
* <p>
|
||||||
|
* 通过该类可向 {@link BotExtensionFactory} 提供更多创建 BotExtension 时可用的参数.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class BotExtensionCreateOptions {
|
||||||
|
|
||||||
|
private final long botAccountId;
|
||||||
|
private final ProxyConfig proxy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造新的 BotExtensionCreateOptions.
|
||||||
|
*
|
||||||
|
* @param botAccountId 创建扩展的 Bot 账户 Id.
|
||||||
|
* @param proxy Bot 所使用的代理配置.
|
||||||
|
*/
|
||||||
|
public BotExtensionCreateOptions(long botAccountId, ProxyConfig proxy) {
|
||||||
|
this.botAccountId = botAccountId;
|
||||||
|
this.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Bot 使用的代理信息.
|
||||||
|
*
|
||||||
|
* @return 返回 Bot 中 TelegramClient 所使用的代理配置.
|
||||||
|
*/
|
||||||
|
public ProxyConfig getProxy() {
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Bot 的账户 Id.
|
||||||
|
*
|
||||||
|
* @return 返回 Bot 的账户 Id.
|
||||||
|
*/
|
||||||
|
public long getBotAccountId() {
|
||||||
|
return botAccountId;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
package net.lamgc.scalabot.extension;
|
package net.lamgc.scalabot.extension;
|
||||||
|
|
||||||
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
|
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
|
||||||
import org.telegram.abilitybots.api.util.AbilityExtension;
|
import org.telegram.telegrambots.abilitybots.api.db.DBContext;
|
||||||
|
import org.telegram.telegrambots.abilitybots.api.util.AbilityExtension;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ import java.io.File;
|
|||||||
* 该接口用于为指定的 {@link BaseAbilityBot} 创建扩展.
|
* 该接口用于为指定的 {@link BaseAbilityBot} 创建扩展.
|
||||||
*
|
*
|
||||||
* <p> 由于 AbilityExtension 无法直接获取 {@link BaseAbilityBot} 的
|
* <p> 由于 AbilityExtension 无法直接获取 {@link BaseAbilityBot} 的
|
||||||
* 数据库对象 {@link org.telegram.abilitybots.api.db.DBContext},
|
* 数据库对象 {@link DBContext},
|
||||||
* 所以将通过该接口工厂来创建扩展对象.
|
* 所以将通过该接口工厂来创建扩展对象.
|
||||||
*
|
*
|
||||||
* @author LamGC
|
* @author LamGC
|
||||||
@ -20,7 +21,7 @@ public interface BotExtensionFactory {
|
|||||||
/**
|
/**
|
||||||
* 为给定的 {@link BaseAbilityBot} 对象创建扩展.
|
* 为给定的 {@link BaseAbilityBot} 对象创建扩展.
|
||||||
*
|
*
|
||||||
* <p> 如扩展无使用 {@link org.telegram.abilitybots.api.db.DBContext} 的话,
|
* <p> 如扩展无使用 {@link DBContext} 的话,
|
||||||
* 也可以返回扩展单例, 因为 AbilityBot 本身并不禁止多个机器人共用一个扩展对象
|
* 也可以返回扩展单例, 因为 AbilityBot 本身并不禁止多个机器人共用一个扩展对象
|
||||||
* (AbilityBot 只是调用了扩展中的方法来创建 Ability 对象).
|
* (AbilityBot 只是调用了扩展中的方法来创建 Ability 对象).
|
||||||
*
|
*
|
||||||
@ -30,8 +31,10 @@ public interface BotExtensionFactory {
|
|||||||
* <pre> $DATA_ROOT/data/extensions/{GroupId}/{ArtifactId}</pre>
|
* <pre> $DATA_ROOT/data/extensions/{GroupId}/{ArtifactId}</pre>
|
||||||
* <b>同一个扩展包的 Factory</b> 接收到的共享数据目录<b>都是一样的</b>,
|
* <b>同一个扩展包的 Factory</b> 接收到的共享数据目录<b>都是一样的</b>,
|
||||||
* 建议将数据存储在数据目录中, 便于数据的存储管理.
|
* 建议将数据存储在数据目录中, 便于数据的存储管理.
|
||||||
|
* @param options 创建扩展时可用的参数.
|
||||||
* @return 返回为该 Bot 对象创建的扩展对象, 如果不希望为该机器人提供扩展, 可返回 {@code null}.
|
* @return 返回为该 Bot 对象创建的扩展对象, 如果不希望为该机器人提供扩展, 可返回 {@code null}.
|
||||||
|
* @since 0.7.0
|
||||||
*/
|
*/
|
||||||
AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder);
|
AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder, BotExtensionCreateOptions options);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,17 @@
|
|||||||
package net.lamgc.scalabot.extension.util;
|
package net.lamgc.scalabot.extension.util;
|
||||||
|
|
||||||
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
|
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class AbilityBots {
|
/**
|
||||||
|
* 一些开发扩展中可以用到的工具类.
|
||||||
private final static Pattern botTokenPattern = Pattern.compile("([1-9]\\d+):([A-Za-z\\d_-]{35,})");
|
*/
|
||||||
|
public final class AbilityBots {
|
||||||
|
|
||||||
private AbilityBots() {
|
private AbilityBots() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 AbilityBot 的账户 Id.
|
|
||||||
*
|
|
||||||
* <p> 账户 Id 来自于 botToken 中, token 的格式为 "[AccountId]:[Secret]".
|
|
||||||
* <p> 账户 Id 的真实性与 botToken 的有效性有关, 本方法并不会确保 botToken 的有效性, 一般情况下也无需考虑 Id 的有效性,
|
|
||||||
* 如果有需要, 可尝试通过调用 {@link org.telegram.telegrambots.meta.api.methods.GetMe} 来确保 botToken 的有效性.
|
|
||||||
*
|
|
||||||
* @param bot 要获取账户 Id 的 AbilityBot 对象.
|
|
||||||
* @return 返回 AbilityBot 的账户 Id.
|
|
||||||
* @throws IllegalArgumentException 当 AbilityBot 的 botToken 格式错误时抛出该异常.
|
|
||||||
*/
|
|
||||||
public static long getBotAccountId(BaseAbilityBot bot) {
|
|
||||||
String botToken = bot.getBotToken();
|
|
||||||
Matcher matcher = botTokenPattern.matcher(botToken);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
throw new IllegalArgumentException("Invalid token format.");
|
|
||||||
}
|
|
||||||
return Long.parseLong(matcher.group(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消某一对话的状态机.
|
* 取消某一对话的状态机.
|
||||||
*
|
*
|
||||||
@ -41,7 +20,7 @@ public class AbilityBots {
|
|||||||
* @return 如果状态机存在, 则删除后返回 true, 不存在(未开启任何状态机, 即没有触发任何 Reply)则返回 false.
|
* @return 如果状态机存在, 则删除后返回 true, 不存在(未开启任何状态机, 即没有触发任何 Reply)则返回 false.
|
||||||
*/
|
*/
|
||||||
public static boolean cancelReplyState(BaseAbilityBot bot, long chatId) {
|
public static boolean cancelReplyState(BaseAbilityBot bot, long chatId) {
|
||||||
Map<Long, Integer> stateMap = bot.db().getMap("user_state_replies");
|
Map<Long, Integer> stateMap = bot.getDb().getMap("user_state_replies");
|
||||||
if (!stateMap.containsKey(chatId)) {
|
if (!stateMap.containsKey(chatId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,35 @@ package net.lamgc.scalabot.extension.util;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mapdb.DBMaker;
|
import org.mapdb.DBMaker;
|
||||||
import org.telegram.abilitybots.api.bot.AbilityBot;
|
import org.telegram.telegrambots.abilitybots.api.bot.AbilityBot;
|
||||||
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
|
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
|
||||||
import org.telegram.abilitybots.api.db.MapDBContext;
|
import org.telegram.telegrambots.abilitybots.api.db.MapDBContext;
|
||||||
import org.telegram.abilitybots.api.objects.*;
|
import org.telegram.telegrambots.abilitybots.api.objects.*;
|
||||||
import org.telegram.abilitybots.api.sender.SilentSender;
|
import org.telegram.telegrambots.abilitybots.api.sender.SilentSender;
|
||||||
import org.telegram.telegrambots.meta.api.objects.Message;
|
|
||||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||||
import org.telegram.telegrambots.meta.api.objects.User;
|
import org.telegram.telegrambots.meta.api.objects.User;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.message.Message;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
public class AbilityBotsTest {
|
public class AbilityBotsTest {
|
||||||
|
|
||||||
public static final User USER = new User(1L, "first", false, "last", "username", null, false, false, false);
|
public static final User USER = User.builder()
|
||||||
public static final User CREATOR = new User(1337L, "creatorFirst", false, "creatorLast", "creatorUsername", null, false, false, false);
|
.userName("username")
|
||||||
|
.id(1L)
|
||||||
|
.firstName("first")
|
||||||
|
.lastName("last")
|
||||||
|
.isBot(false)
|
||||||
|
.build();
|
||||||
|
public static final User CREATOR = User.builder()
|
||||||
|
.userName("creatorUsername")
|
||||||
|
.id(1337L)
|
||||||
|
.firstName("creatorFirst")
|
||||||
|
.lastName("creatorLast")
|
||||||
|
.isBot(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
static Update mockFullUpdate(BaseAbilityBot bot, User user, String args) {
|
static Update mockFullUpdate(BaseAbilityBot bot, User user, String args) {
|
||||||
bot.users().put(USER.getId(), USER);
|
bot.users().put(USER.getId(), USER);
|
||||||
@ -39,73 +52,67 @@ public class AbilityBotsTest {
|
|||||||
return update;
|
return update;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void getBotAccountIdTest() {
|
|
||||||
String expectToken = "1234567890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
|
|
||||||
long actual = AbilityBots.getBotAccountId(new TestingAbilityBot(expectToken, "test"));
|
|
||||||
assertEquals(1234567890, actual);
|
|
||||||
|
|
||||||
String badTokenA = "12c34d56a7890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
|
|
||||||
assertThrows(IllegalArgumentException.class, () ->
|
|
||||||
AbilityBots.getBotAccountId(new TestingAbilityBot(badTokenA, "test")));
|
|
||||||
|
|
||||||
String badTokenB = "12c34d56a7890AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
|
|
||||||
assertThrows(IllegalArgumentException.class, () ->
|
|
||||||
AbilityBots.getBotAccountId(new TestingAbilityBot(badTokenB, "test")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void cancelReplyStateTest() {
|
void cancelReplyStateTest() {
|
||||||
User userA = new User(10001L, "first", false, "last", "username", null, false, false, false);
|
User userA = User.builder()
|
||||||
User userB = new User(10101L, "first", false, "last", "username", null, false, false, false);
|
.id(10001L)
|
||||||
|
.firstName("first")
|
||||||
|
.lastName("last")
|
||||||
|
.userName("username")
|
||||||
|
.isBot(false)
|
||||||
|
.build();
|
||||||
|
User userB = User.builder()
|
||||||
|
.id(10101L)
|
||||||
|
.firstName("first")
|
||||||
|
.lastName("last")
|
||||||
|
.userName("username")
|
||||||
|
.isBot(false)
|
||||||
|
.build();
|
||||||
SilentSender silent = mock(SilentSender.class);
|
SilentSender silent = mock(SilentSender.class);
|
||||||
BaseAbilityBot bot = new TestingAbilityBot("", "", silent);
|
BaseAbilityBot bot = new TestingAbilityBot("", silent);
|
||||||
bot.onRegister();
|
bot.onRegister();
|
||||||
bot.onUpdateReceived(mockFullUpdate(bot, userA, "/set_reply"));
|
bot.consume(mockFullUpdate(bot, userA, "/set_reply"));
|
||||||
verify(silent, times(1)).send("Reply set!", userA.getId());
|
verify(silent, times(1)).send("Reply set!", userA.getId());
|
||||||
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_01"));
|
bot.consume(mockFullUpdate(bot, userA, "reply_01"));
|
||||||
verify(silent, times(1)).send("Reply 01", userA.getId());
|
verify(silent, times(1)).send("Reply 01", userA.getId());
|
||||||
assertTrue(AbilityBots.cancelReplyState(bot, userA.getId()));
|
assertTrue(AbilityBots.cancelReplyState(bot, userA.getId()));
|
||||||
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_02"));
|
bot.consume(mockFullUpdate(bot, userA, "reply_02"));
|
||||||
verify(silent, never()).send("Reply 02", userA.getId());
|
verify(silent, never()).send("Reply 02", userA.getId());
|
||||||
|
|
||||||
assertFalse(AbilityBots.cancelReplyState(bot, userB.getId()));
|
assertFalse(AbilityBots.cancelReplyState(bot, userB.getId()));
|
||||||
|
|
||||||
silent = mock(SilentSender.class);
|
silent = mock(SilentSender.class);
|
||||||
bot = new TestingAbilityBot("", "", silent);
|
bot = new TestingAbilityBot("", silent);
|
||||||
bot.onRegister();
|
bot.onRegister();
|
||||||
|
|
||||||
bot.onUpdateReceived(mockFullUpdate(bot, userA, "/set_reply"));
|
bot.consume(mockFullUpdate(bot, userA, "/set_reply"));
|
||||||
verify(silent, times(1)).send("Reply set!", userA.getId());
|
verify(silent, times(1)).send("Reply set!", userA.getId());
|
||||||
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_01"));
|
bot.consume(mockFullUpdate(bot, userA, "reply_01"));
|
||||||
verify(silent, times(1)).send("Reply 01", userA.getId());
|
verify(silent, times(1)).send("Reply 01", userA.getId());
|
||||||
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_02"));
|
bot.consume(mockFullUpdate(bot, userA, "reply_02"));
|
||||||
verify(silent, times(1)).send("Reply 02", userA.getId());
|
verify(silent, times(1)).send("Reply 02", userA.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class TestingAbilityBot extends AbilityBot {
|
public static class TestingAbilityBot extends AbilityBot {
|
||||||
|
|
||||||
public TestingAbilityBot(String botToken, String botUsername) {
|
public TestingAbilityBot(String botUsername, SilentSender silentSender) {
|
||||||
super(botToken, botUsername, new MapDBContext(DBMaker.heapDB().make()));
|
super(new NoOpTelegramClient(), botUsername, new MapDBContext(DBMaker.heapDB().make()));
|
||||||
}
|
|
||||||
|
|
||||||
public TestingAbilityBot(String botToken, String botUsername, SilentSender silentSender) {
|
|
||||||
super(botToken, botUsername, new MapDBContext(DBMaker.heapDB().make()));
|
|
||||||
this.silent = silentSender;
|
this.silent = silentSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public Ability setReply() {
|
public Ability setReply() {
|
||||||
return Ability.builder()
|
return Ability.builder()
|
||||||
.name("set_reply")
|
.name("set_reply")
|
||||||
.enableStats()
|
.enableStats()
|
||||||
.locality(Locality.ALL)
|
.locality(Locality.ALL)
|
||||||
.privacy(Privacy.PUBLIC)
|
.privacy(Privacy.PUBLIC)
|
||||||
.action(ctx -> ctx.bot().silent().send("Reply set!", ctx.chatId()))
|
.action(ctx -> ctx.bot().getSilent().send("Reply set!", ctx.chatId()))
|
||||||
.reply(ReplyFlow.builder(db())
|
.reply(ReplyFlow.builder(getDb())
|
||||||
.action((bot, upd) -> bot.silent().send("Reply 01", upd.getMessage().getChatId()))
|
.action((bot, upd) -> bot.getSilent().send("Reply 01", upd.getMessage().getChatId()))
|
||||||
.onlyIf(upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_01"))
|
.onlyIf(upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_01"))
|
||||||
.next(Reply.of((bot, upd) ->
|
.next(Reply.of((bot, upd) ->
|
||||||
bot.silent().send("Reply 02", upd.getMessage().getChatId()),
|
bot.getSilent().send("Reply 02", upd.getMessage().getChatId()),
|
||||||
upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_02")))
|
upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_02")))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
package net.lamgc.scalabot.extension.util;
|
||||||
|
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.botapimethods.BotApiMethod;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.groupadministration.SetChatPhoto;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.send.*;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.stickers.*;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageMedia;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.File;
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.message.Message;
|
||||||
|
import org.telegram.telegrambots.meta.generics.TelegramClient;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
class NoOpTelegramClient implements TelegramClient {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Serializable, Method extends BotApiMethod<T>> CompletableFuture<T> executeAsync(Method method) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Serializable, Method extends BotApiMethod<T>> T execute(Method method) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendDocument sendDocument) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendPhoto sendPhoto) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean execute(SetWebhook setWebhook) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendVideo sendVideo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendVideoNote sendVideoNote) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendSticker sendSticker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendAudio sendAudio) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendVoice sendVoice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Message> execute(SendMediaGroup sendMediaGroup) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Message> execute(SendPaidMedia sendPaidMedia) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean execute(SetChatPhoto setChatPhoto) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean execute(AddStickerToSet addStickerToSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean execute(ReplaceStickerInSet replaceStickerInSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean execute(SetStickerSetThumbnail setStickerSetThumbnail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean execute(CreateNewStickerSet createNewStickerSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File execute(UploadStickerFile uploadStickerFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Serializable execute(EditMessageMedia editMessageMedia) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.io.File downloadFile(File file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream downloadFileAsStream(File file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message execute(SendAnimation sendAnimation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendDocument sendDocument) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendPhoto sendPhoto) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> executeAsync(SetWebhook setWebhook) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendVideo sendVideo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendVideoNote sendVideoNote) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendSticker sendSticker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendAudio sendAudio) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendVoice sendVoice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<Message>> executeAsync(SendMediaGroup sendMediaGroup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<Message>> executeAsync(SendPaidMedia sendPaidMedia) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> executeAsync(SetChatPhoto setChatPhoto) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> executeAsync(AddStickerToSet addStickerToSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> executeAsync(ReplaceStickerInSet replaceStickerInSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> executeAsync(SetStickerSetThumbnail setStickerSetThumbnail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> executeAsync(CreateNewStickerSet createNewStickerSet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<File> executeAsync(UploadStickerFile uploadStickerFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Serializable> executeAsync(EditMessageMedia editMessageMedia) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Message> executeAsync(SendAnimation sendAnimation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<java.io.File> downloadFileAsync(File file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<InputStream> downloadFileAsStreamAsync(File file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
13
scalabot-meta/README.md
Normal file
13
scalabot-meta/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# scalabot-meta
|
||||||
|
|
||||||
|
本模块用于将 ScalaBot 的一些配置相关内容发布出去,以便于其他项目使用。
|
||||||
|
|
||||||
|
主要是配置类和相应的 Gson 序列化器(如果有,或者必要)。
|
||||||
|
|
||||||
|
## 关于序列化器
|
||||||
|
|
||||||
|
强烈建议使用序列化器!由于 Kotlin 与 Gson 之间的一些兼容性问题
|
||||||
|
(参见[本提交](https://github.com/LamGC/ScalaBot/commit/084280564af58d1af22db5b57c67577d93bd820e)),
|
||||||
|
如果直接让 Gson 解析 Kotlin Data 类,将会出现一些潜在的问题(比如无法使用默认值)。
|
||||||
|
部分序列化器也可以帮助检查字段值是否合法,以防止因字段值不正确导致出现更多的问题
|
||||||
|
(例如 BotAccount 中,如果 `token` 的格式有误,那么获取 `id` 时将引发 `NumberFormatException` 异常)。
|
201
scalabot-meta/api/scalabot-meta.api
Normal file
201
scalabot-meta/api/scalabot-meta.api
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
public final class net/lamgc/scalabot/config/AppConfig {
|
||||||
|
public fun <init> ()V
|
||||||
|
public fun <init> (Lnet/lamgc/scalabot/config/ProxyConfig;Lnet/lamgc/scalabot/config/MetricsConfig;Ljava/util/List;Ljava/lang/String;)V
|
||||||
|
public synthetic fun <init> (Lnet/lamgc/scalabot/config/ProxyConfig;Lnet/lamgc/scalabot/config/MetricsConfig;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public final fun component1 ()Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public final fun component2 ()Lnet/lamgc/scalabot/config/MetricsConfig;
|
||||||
|
public final fun component3 ()Ljava/util/List;
|
||||||
|
public final fun component4 ()Ljava/lang/String;
|
||||||
|
public final fun copy (Lnet/lamgc/scalabot/config/ProxyConfig;Lnet/lamgc/scalabot/config/MetricsConfig;Ljava/util/List;Ljava/lang/String;)Lnet/lamgc/scalabot/config/AppConfig;
|
||||||
|
public static synthetic fun copy$default (Lnet/lamgc/scalabot/config/AppConfig;Lnet/lamgc/scalabot/config/ProxyConfig;Lnet/lamgc/scalabot/config/MetricsConfig;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lnet/lamgc/scalabot/config/AppConfig;
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public final fun getMavenLocalRepository ()Ljava/lang/String;
|
||||||
|
public final fun getMavenRepositories ()Ljava/util/List;
|
||||||
|
public final fun getMetrics ()Lnet/lamgc/scalabot/config/MetricsConfig;
|
||||||
|
public final fun getProxy ()Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/BotAccount {
|
||||||
|
public fun <init> (Ljava/lang/String;Ljava/lang/String;J)V
|
||||||
|
public final fun component1 ()Ljava/lang/String;
|
||||||
|
public final fun component2 ()Ljava/lang/String;
|
||||||
|
public final fun component3 ()J
|
||||||
|
public final fun copy (Ljava/lang/String;Ljava/lang/String;J)Lnet/lamgc/scalabot/config/BotAccount;
|
||||||
|
public static synthetic fun copy$default (Lnet/lamgc/scalabot/config/BotAccount;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lnet/lamgc/scalabot/config/BotAccount;
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public final fun getCreatorId ()J
|
||||||
|
public final fun getId ()J
|
||||||
|
public final fun getName ()Ljava/lang/String;
|
||||||
|
public final fun getToken ()Ljava/lang/String;
|
||||||
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/BotConfig {
|
||||||
|
public fun <init> (ZLnet/lamgc/scalabot/config/BotAccount;ZZLjava/util/Set;Lnet/lamgc/scalabot/config/ProxyConfig;Ljava/lang/String;)V
|
||||||
|
public synthetic fun <init> (ZLnet/lamgc/scalabot/config/BotAccount;ZZLjava/util/Set;Lnet/lamgc/scalabot/config/ProxyConfig;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public final fun component1 ()Z
|
||||||
|
public final fun component2 ()Lnet/lamgc/scalabot/config/BotAccount;
|
||||||
|
public final fun component3 ()Z
|
||||||
|
public final fun component4 ()Z
|
||||||
|
public final fun component5 ()Ljava/util/Set;
|
||||||
|
public final fun component6 ()Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public final fun component7 ()Ljava/lang/String;
|
||||||
|
public final fun copy (ZLnet/lamgc/scalabot/config/BotAccount;ZZLjava/util/Set;Lnet/lamgc/scalabot/config/ProxyConfig;Ljava/lang/String;)Lnet/lamgc/scalabot/config/BotConfig;
|
||||||
|
public static synthetic fun copy$default (Lnet/lamgc/scalabot/config/BotConfig;ZLnet/lamgc/scalabot/config/BotAccount;ZZLjava/util/Set;Lnet/lamgc/scalabot/config/ProxyConfig;Ljava/lang/String;ILjava/lang/Object;)Lnet/lamgc/scalabot/config/BotConfig;
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public final fun getAccount ()Lnet/lamgc/scalabot/config/BotAccount;
|
||||||
|
public final fun getAutoUpdateCommandList ()Z
|
||||||
|
public final fun getBaseApiTelegramUrl ()Lorg/telegram/telegrambots/meta/TelegramUrl;
|
||||||
|
public final fun getBaseApiUrl ()Ljava/lang/String;
|
||||||
|
public final fun getDisableBuiltInAbility ()Z
|
||||||
|
public final fun getEnabled ()Z
|
||||||
|
public final fun getExtensions ()Ljava/util/Set;
|
||||||
|
public final fun getProxy ()Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/ConfigsKt {
|
||||||
|
public static final fun getDefaultTelegramApiUrl ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/MavenRepositoryConfig {
|
||||||
|
public fun <init> (Ljava/lang/String;Ljava/net/URL;Lorg/eclipse/aether/repository/Proxy;Ljava/lang/String;ZZLorg/eclipse/aether/repository/Authentication;)V
|
||||||
|
public synthetic fun <init> (Ljava/lang/String;Ljava/net/URL;Lorg/eclipse/aether/repository/Proxy;Ljava/lang/String;ZZLorg/eclipse/aether/repository/Authentication;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public final fun component1 ()Ljava/lang/String;
|
||||||
|
public final fun component2 ()Ljava/net/URL;
|
||||||
|
public final fun component3 ()Lorg/eclipse/aether/repository/Proxy;
|
||||||
|
public final fun component4 ()Ljava/lang/String;
|
||||||
|
public final fun component5 ()Z
|
||||||
|
public final fun component6 ()Z
|
||||||
|
public final fun component7 ()Lorg/eclipse/aether/repository/Authentication;
|
||||||
|
public final fun copy (Ljava/lang/String;Ljava/net/URL;Lorg/eclipse/aether/repository/Proxy;Ljava/lang/String;ZZLorg/eclipse/aether/repository/Authentication;)Lnet/lamgc/scalabot/config/MavenRepositoryConfig;
|
||||||
|
public static synthetic fun copy$default (Lnet/lamgc/scalabot/config/MavenRepositoryConfig;Ljava/lang/String;Ljava/net/URL;Lorg/eclipse/aether/repository/Proxy;Ljava/lang/String;ZZLorg/eclipse/aether/repository/Authentication;ILjava/lang/Object;)Lnet/lamgc/scalabot/config/MavenRepositoryConfig;
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public final fun getAuthentication ()Lorg/eclipse/aether/repository/Authentication;
|
||||||
|
public final fun getEnableReleases ()Z
|
||||||
|
public final fun getEnableSnapshots ()Z
|
||||||
|
public final fun getId ()Ljava/lang/String;
|
||||||
|
public final fun getLayout ()Ljava/lang/String;
|
||||||
|
public final fun getProxy ()Lorg/eclipse/aether/repository/Proxy;
|
||||||
|
public final fun getUrl ()Ljava/net/URL;
|
||||||
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/MetricsConfig {
|
||||||
|
public fun <init> ()V
|
||||||
|
public fun <init> (ZILjava/lang/String;Lnet/lamgc/scalabot/config/UsernameAuthenticator;)V
|
||||||
|
public synthetic fun <init> (ZILjava/lang/String;Lnet/lamgc/scalabot/config/UsernameAuthenticator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public final fun component1 ()Z
|
||||||
|
public final fun component2 ()I
|
||||||
|
public final fun component3 ()Ljava/lang/String;
|
||||||
|
public final fun component4 ()Lnet/lamgc/scalabot/config/UsernameAuthenticator;
|
||||||
|
public final fun copy (ZILjava/lang/String;Lnet/lamgc/scalabot/config/UsernameAuthenticator;)Lnet/lamgc/scalabot/config/MetricsConfig;
|
||||||
|
public static synthetic fun copy$default (Lnet/lamgc/scalabot/config/MetricsConfig;ZILjava/lang/String;Lnet/lamgc/scalabot/config/UsernameAuthenticator;ILjava/lang/Object;)Lnet/lamgc/scalabot/config/MetricsConfig;
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public final fun getAuthenticator ()Lnet/lamgc/scalabot/config/UsernameAuthenticator;
|
||||||
|
public final fun getBindAddress ()Ljava/lang/String;
|
||||||
|
public final fun getEnable ()Z
|
||||||
|
public final fun getPort ()I
|
||||||
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/ProxyConfig {
|
||||||
|
public fun <init> ()V
|
||||||
|
public fun <init> (Lnet/lamgc/scalabot/config/ProxyType;Ljava/lang/String;I)V
|
||||||
|
public synthetic fun <init> (Lnet/lamgc/scalabot/config/ProxyType;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public final fun component1 ()Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public final fun component2 ()Ljava/lang/String;
|
||||||
|
public final fun component3 ()I
|
||||||
|
public final fun copy (Lnet/lamgc/scalabot/config/ProxyType;Ljava/lang/String;I)Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public static synthetic fun copy$default (Lnet/lamgc/scalabot/config/ProxyConfig;Lnet/lamgc/scalabot/config/ProxyType;Ljava/lang/String;IILjava/lang/Object;)Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public final fun getHost ()Ljava/lang/String;
|
||||||
|
public final fun getPort ()I
|
||||||
|
public final fun getType ()Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/ProxyType : java/lang/Enum {
|
||||||
|
public static final field HTTP Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public static final field HTTPS Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public static final field NO_PROXY Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public static final field SOCKS4 Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public static final field SOCKS5 Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public static fun getEntries ()Lkotlin/enums/EnumEntries;
|
||||||
|
public static fun valueOf (Ljava/lang/String;)Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public static fun values ()[Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/UsernameAuthenticator : com/sun/net/httpserver/BasicAuthenticator {
|
||||||
|
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
||||||
|
public fun checkCredentials (Ljava/lang/String;Ljava/lang/String;)Z
|
||||||
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
|
public fun hashCode ()I
|
||||||
|
public final fun toJsonObject ()Lcom/google/gson/JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/ArtifactSerializer : com/google/gson/JsonDeserializer, com/google/gson/JsonSerializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/ArtifactSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lorg/eclipse/aether/artifact/Artifact;
|
||||||
|
public synthetic fun serialize (Ljava/lang/Object;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
public fun serialize (Lorg/eclipse/aether/artifact/Artifact;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/AuthenticationSerializer : com/google/gson/JsonDeserializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/AuthenticationSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lorg/eclipse/aether/repository/Authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/BotAccountSerializer : com/google/gson/JsonDeserializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/BotAccountSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/lamgc/scalabot/config/BotAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/BotConfigSerializer : com/google/gson/JsonDeserializer, com/google/gson/JsonSerializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/BotConfigSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/lamgc/scalabot/config/BotConfig;
|
||||||
|
public synthetic fun serialize (Ljava/lang/Object;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
public fun serialize (Lnet/lamgc/scalabot/config/BotConfig;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/MavenRepositoryConfigSerializer : com/google/gson/JsonDeserializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/MavenRepositoryConfigSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/lamgc/scalabot/config/MavenRepositoryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/ProxyConfigSerializer : com/google/gson/JsonDeserializer, com/google/gson/JsonSerializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/ProxyConfigSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/lamgc/scalabot/config/ProxyConfig;
|
||||||
|
public synthetic fun serialize (Ljava/lang/Object;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
public fun serialize (Lnet/lamgc/scalabot/config/ProxyConfig;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/ProxyTypeSerializer : com/google/gson/JsonDeserializer, com/google/gson/JsonSerializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/ProxyTypeSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/lamgc/scalabot/config/ProxyType;
|
||||||
|
public synthetic fun serialize (Ljava/lang/Object;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
public fun serialize (Lnet/lamgc/scalabot/config/ProxyType;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class net/lamgc/scalabot/config/serializer/UsernameAuthenticatorSerializer : com/google/gson/JsonDeserializer, com/google/gson/JsonSerializer {
|
||||||
|
public static final field INSTANCE Lnet/lamgc/scalabot/config/serializer/UsernameAuthenticatorSerializer;
|
||||||
|
public synthetic fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Ljava/lang/Object;
|
||||||
|
public fun deserialize (Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/lamgc/scalabot/config/UsernameAuthenticator;
|
||||||
|
public synthetic fun serialize (Ljava/lang/Object;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
public fun serialize (Lnet/lamgc/scalabot/config/UsernameAuthenticator;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;
|
||||||
|
}
|
||||||
|
|
107
scalabot-meta/build.gradle.kts
Normal file
107
scalabot-meta/build.gradle.kts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
id("org.jetbrains.kotlinx.kover")
|
||||||
|
id("org.jetbrains.dokka") version "1.9.20"
|
||||||
|
`maven-publish`
|
||||||
|
signing
|
||||||
|
id("org.jetbrains.kotlinx.binary-compatibility-validator")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val aetherVersion = "1.1.0"
|
||||||
|
api("org.eclipse.aether:aether-api:$aetherVersion")
|
||||||
|
implementation("org.eclipse.aether:aether-util:$aetherVersion")
|
||||||
|
|
||||||
|
implementation("org.telegram:telegrambots-meta:8.0.0")
|
||||||
|
|
||||||
|
api("com.google.code.gson:gson:2.11.0")
|
||||||
|
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
testImplementation("io.mockk:mockk:1.13.13")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
|
||||||
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1")
|
||||||
|
|
||||||
|
dokkaHtmlPlugin("org.jetbrains.dokka:javadoc-plugin:2.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withJavadocJar()
|
||||||
|
withSourcesJar()
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||||
|
isPreserveFileTimestamps = false
|
||||||
|
isReproducibleFileOrder = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.getByName<Test>("test") {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
val javadocJar = tasks.named<Jar>("javadocJar") {
|
||||||
|
from(tasks.named("dokkaJavadoc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
maven("https://git.lamgc.me/api/packages/LamGC/maven") {
|
||||||
|
credentials {
|
||||||
|
username = project.properties["repo.credentials.self-git.username"].toString()
|
||||||
|
password = project.properties["repo.credentials.self-git.password"].toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("maven") {
|
||||||
|
from(components["kotlin"])
|
||||||
|
artifact(javadocJar)
|
||||||
|
artifact(tasks.named("sourcesJar"))
|
||||||
|
pom {
|
||||||
|
name.set("ScalaBot-meta")
|
||||||
|
description.set(
|
||||||
|
"Shared components used by scalabot (such as configuration classes)"
|
||||||
|
)
|
||||||
|
url.set("https://github.com/LamGC/ScalaBot")
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name.set("The MIT License")
|
||||||
|
url.set("https://www.opensource.org/licenses/mit-license.php")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id.set("LamGC")
|
||||||
|
name.set("LamGC")
|
||||||
|
email.set("lam827@lamgc.net")
|
||||||
|
url.set("https://github.com/LamGC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection.set("scm:git:https://github.com/LamGC/ScalaBot.git")
|
||||||
|
developerConnection.set("scm:git:https://github.com/LamGC/ScalaBot.git")
|
||||||
|
url.set("https://github.com/LamGC/ScalaBot")
|
||||||
|
}
|
||||||
|
issueManagement {
|
||||||
|
url.set("https://github.com/LamGC/ScalaBot/issues")
|
||||||
|
system.set("Github Issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
sign(publishing.publications["maven"])
|
||||||
|
}
|
175
scalabot-meta/src/main/kotlin/Configs.kt
Normal file
175
scalabot-meta/src/main/kotlin/Configs.kt
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import org.eclipse.aether.artifact.Artifact
|
||||||
|
import org.eclipse.aether.repository.Authentication
|
||||||
|
import org.eclipse.aether.repository.Proxy
|
||||||
|
import org.telegram.telegrambots.meta.TelegramUrl
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器人帐号信息.
|
||||||
|
* @property name 机器人名称, 建议与实际设定的名称相同.
|
||||||
|
* @property token 机器人 API Token.
|
||||||
|
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
|
||||||
|
* @property id 机器人账号 ID.
|
||||||
|
*/
|
||||||
|
data class BotAccount(
|
||||||
|
val name: String,
|
||||||
|
val token: String,
|
||||||
|
val creatorId: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
val id
|
||||||
|
// 不要想着每次获取都要从 token 里取出有性能损耗.
|
||||||
|
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
|
||||||
|
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
|
||||||
|
get() = token.substringBefore(":").toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultTelegramApiUrl: String = URL(
|
||||||
|
TelegramUrl.DEFAULT_URL.schema,
|
||||||
|
TelegramUrl.DEFAULT_URL.host,
|
||||||
|
TelegramUrl.DEFAULT_URL.port,
|
||||||
|
"/"
|
||||||
|
).toExternalForm()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器人配置.
|
||||||
|
*
|
||||||
|
* 使用 Gson 解析时, 请添加以下类型适配器:
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.ProxyTypeSerializer]
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.BotConfigSerializer]
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.BotAccountSerializer]
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.ArtifactSerializer]
|
||||||
|
*
|
||||||
|
* @property enabled 是否启用机器人.
|
||||||
|
* @property account 机器人帐号信息, 用于访问 API.
|
||||||
|
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
|
||||||
|
* @property autoUpdateCommandList 是否自动更新机器人在 Telegram 的命令列表.
|
||||||
|
* @property extensions 该机器人启用的扩展.
|
||||||
|
* @property proxy 为该机器人单独设置的代理配置, 如无设置, 则使用 AppConfig 中的代理配置.
|
||||||
|
* @property baseApiUrl 机器人所使用的 API 地址, 适用于自建 Telegram Bot API 端点.
|
||||||
|
*/
|
||||||
|
data class BotConfig(
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
val account: BotAccount,
|
||||||
|
val disableBuiltInAbility: Boolean = false,
|
||||||
|
val autoUpdateCommandList: Boolean = false,
|
||||||
|
/*
|
||||||
|
* 使用构件坐标来选择机器人所使用的扩展包.
|
||||||
|
* 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id,
|
||||||
|
* 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目时一定会设置的,
|
||||||
|
* 所以就直接用了. :P
|
||||||
|
*/
|
||||||
|
val extensions: Set<Artifact> = emptySet(),
|
||||||
|
val proxy: ProxyConfig = ProxyConfig(type = ProxyType.NO_PROXY),
|
||||||
|
val baseApiUrl: String = defaultTelegramApiUrl
|
||||||
|
) {
|
||||||
|
fun getBaseApiTelegramUrl(): TelegramUrl {
|
||||||
|
if (this.baseApiUrl == defaultTelegramApiUrl) {
|
||||||
|
return TelegramUrl.DEFAULT_URL
|
||||||
|
} else {
|
||||||
|
URI.create(baseApiUrl).let {
|
||||||
|
return TelegramUrl.builder()
|
||||||
|
.host(it.host)
|
||||||
|
.port(it.port)
|
||||||
|
.schema(it.scheme)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理类型.
|
||||||
|
*/
|
||||||
|
enum class ProxyType {
|
||||||
|
NO_PROXY,
|
||||||
|
HTTP,
|
||||||
|
HTTPS,
|
||||||
|
SOCKS4,
|
||||||
|
SOCKS5
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理配置.
|
||||||
|
* @property type 代理类型.
|
||||||
|
* @property host 代理服务端地址.
|
||||||
|
* @property port 代理服务端端口.
|
||||||
|
*/
|
||||||
|
data class ProxyConfig(
|
||||||
|
val type: ProxyType = ProxyType.NO_PROXY,
|
||||||
|
val host: String = "127.0.0.1",
|
||||||
|
val port: Int = 1080,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return if (type != ProxyType.NO_PROXY) {
|
||||||
|
"$type://$host:$port"
|
||||||
|
} else {
|
||||||
|
"NO_PROXY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScalaBot 的运行指标公开配置.
|
||||||
|
*
|
||||||
|
* ScalaBot 内置了用于公开运行指标的服务端,
|
||||||
|
* 该指标遵循 Prometheus 的标准, 可以通过 Prometheus 的工具来查看.
|
||||||
|
*
|
||||||
|
* @property enable 是否启用运行指标服务端.
|
||||||
|
* @property port 运行指标服务端的端口.
|
||||||
|
* @property bindAddress 运行指标服务端的绑定地址, 绑定后只有该地址可以访问.
|
||||||
|
* @property authenticator 运行指标服务端的 HTTP 认证配置.
|
||||||
|
*/
|
||||||
|
data class MetricsConfig(
|
||||||
|
val enable: Boolean = false,
|
||||||
|
val port: Int = 9386,
|
||||||
|
val bindAddress: String? = "0.0.0.0",
|
||||||
|
val authenticator: UsernameAuthenticator? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maven 远端仓库配置.
|
||||||
|
* @property id 远端仓库 ID, 如果该属性未配置 (null), 那么运行时将会自动分配一个 Id.
|
||||||
|
* @property url 仓库地址.
|
||||||
|
* @property proxy 访问仓库所使用的代理, 仅支持 http/https 代理.
|
||||||
|
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
|
||||||
|
* @property enableReleases 是否在该远端仓库获取发布版本.
|
||||||
|
* @property enableSnapshots 是否在该远端仓库获取快照版本.
|
||||||
|
* @property authentication 访问该远端仓库所使用的认证配置.
|
||||||
|
*/
|
||||||
|
data class MavenRepositoryConfig(
|
||||||
|
val id: String? = null,
|
||||||
|
val url: URL,
|
||||||
|
val proxy: Proxy? = null,
|
||||||
|
val layout: String = "default",
|
||||||
|
val enableReleases: Boolean = true,
|
||||||
|
val enableSnapshots: Boolean = true,
|
||||||
|
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
|
||||||
|
val authentication: Authentication? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScalaBot App 配置.
|
||||||
|
*
|
||||||
|
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
|
||||||
|
*
|
||||||
|
* 使用 Gson 解析时, 请添加以下类型适配器:
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.ProxyTypeSerializer]
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.MavenRepositoryConfigSerializer]
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.AuthenticationSerializer]
|
||||||
|
* - [net.lamgc.scalabot.config.serializer.UsernameAuthenticatorSerializer]
|
||||||
|
*
|
||||||
|
* @property proxy Telegram API 代理配置.
|
||||||
|
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
|
||||||
|
* @property mavenRepositories Maven 远端仓库配置.
|
||||||
|
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
|
||||||
|
*/
|
||||||
|
data class AppConfig(
|
||||||
|
val proxy: ProxyConfig = ProxyConfig(),
|
||||||
|
val metrics: MetricsConfig = MetricsConfig(),
|
||||||
|
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
|
||||||
|
val mavenLocalRepository: String? = null
|
||||||
|
)
|
26
scalabot-meta/src/main/kotlin/UsernameAuthenticator.kt
Normal file
26
scalabot-meta/src/main/kotlin/UsernameAuthenticator.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.sun.net.httpserver.BasicAuthenticator
|
||||||
|
|
||||||
|
class UsernameAuthenticator(private val username: String, private val password: String) :
|
||||||
|
BasicAuthenticator("metrics") {
|
||||||
|
override fun checkCredentials(username: String?, password: String?): Boolean =
|
||||||
|
this.username == username && this.password == password
|
||||||
|
|
||||||
|
fun toJsonObject(): JsonObject = JsonObject().apply {
|
||||||
|
addProperty("username", username)
|
||||||
|
addProperty("password", password)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other is UsernameAuthenticator && this.username == other.username && this.password == other.password
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = username.hashCode()
|
||||||
|
result = 31 * result + password.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
300
scalabot-meta/src/main/kotlin/serializer/Serializer.kt
Normal file
300
scalabot-meta/src/main/kotlin/serializer/Serializer.kt
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
package net.lamgc.scalabot.config.serializer
|
||||||
|
|
||||||
|
import com.google.gson.*
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import net.lamgc.scalabot.config.*
|
||||||
|
import net.lamgc.scalabot.config.serializer.SerializeUtils.getPrimitiveValueOrThrow
|
||||||
|
import org.eclipse.aether.artifact.AbstractArtifact
|
||||||
|
import org.eclipse.aether.artifact.Artifact
|
||||||
|
import org.eclipse.aether.artifact.DefaultArtifact
|
||||||
|
import org.eclipse.aether.repository.Authentication
|
||||||
|
import org.eclipse.aether.repository.Proxy
|
||||||
|
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object ProxyTypeSerializer : JsonDeserializer<ProxyType>,
|
||||||
|
JsonSerializer<ProxyType> {
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type?,
|
||||||
|
context: JsonDeserializationContext?
|
||||||
|
): ProxyType {
|
||||||
|
if (json.isJsonNull) {
|
||||||
|
return ProxyType.NO_PROXY
|
||||||
|
}
|
||||||
|
if (!json.isJsonPrimitive) {
|
||||||
|
throw JsonParseException("Wrong configuration value type.")
|
||||||
|
}
|
||||||
|
val value = json.asString.trim()
|
||||||
|
try {
|
||||||
|
return ProxyType.valueOf(value.uppercase())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw JsonParseException("Invalid value: $value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
src: ProxyType,
|
||||||
|
typeOfSrc: Type?,
|
||||||
|
context: JsonSerializationContext?
|
||||||
|
): JsonElement {
|
||||||
|
return JsonPrimitive(src.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
|
||||||
|
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||||
|
return if (src is AbstractArtifact) {
|
||||||
|
JsonPrimitive(src.toString())
|
||||||
|
} else {
|
||||||
|
JsonPrimitive(
|
||||||
|
DefaultArtifact(
|
||||||
|
src.groupId,
|
||||||
|
src.artifactId,
|
||||||
|
src.classifier,
|
||||||
|
src.extension,
|
||||||
|
src.version
|
||||||
|
).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
|
||||||
|
if (!json.isJsonPrimitive) {
|
||||||
|
throw JsonParseException("Wrong configuration value type.")
|
||||||
|
}
|
||||||
|
val artifactStr = json.asString.trim()
|
||||||
|
try {
|
||||||
|
return DefaultArtifact(artifactStr)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw JsonParseException("Invalid artifact format: `${artifactStr}`.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object AuthenticationSerializer : JsonDeserializer<Authentication> {
|
||||||
|
|
||||||
|
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication {
|
||||||
|
if (json !is JsonObject) {
|
||||||
|
throw JsonParseException("Unsupported JSON type.")
|
||||||
|
}
|
||||||
|
val username = json.getPrimitiveValueOrThrow("username").asString
|
||||||
|
val password = json.getPrimitiveValueOrThrow("password").asString
|
||||||
|
val builder = AuthenticationBuilder()
|
||||||
|
builder.addUsername(username)
|
||||||
|
builder.addPassword(password)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object SerializeUtils {
|
||||||
|
|
||||||
|
fun JsonObject.getPrimitiveValueOrThrow(fieldName: String): JsonPrimitive {
|
||||||
|
val value = get(fieldName) ?: throw JsonParseException("Missing `$fieldName` field.")
|
||||||
|
if (value !is JsonPrimitive) {
|
||||||
|
throw JsonParseException("Invalid `account` field type.")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object MavenRepositoryConfigSerializer
|
||||||
|
: JsonDeserializer<MavenRepositoryConfig> {
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): MavenRepositoryConfig {
|
||||||
|
return when (json) {
|
||||||
|
is JsonObject -> {
|
||||||
|
MavenRepositoryConfig(
|
||||||
|
id = json.get("id")?.asString,
|
||||||
|
url = URL(json.getPrimitiveValueOrThrow("url").asString),
|
||||||
|
proxy = if (json.has("proxy"))
|
||||||
|
context.deserialize<Proxy>(
|
||||||
|
json.get("proxy"), Proxy::class.java
|
||||||
|
) else null,
|
||||||
|
layout = json.get("layout")?.asString ?: "default",
|
||||||
|
enableReleases = json.get("enableReleases")?.asBoolean ?: true,
|
||||||
|
enableSnapshots = json.get("enableSnapshots")?.asBoolean ?: true,
|
||||||
|
authentication = if (json.has("authentication"))
|
||||||
|
context.deserialize<Authentication>(
|
||||||
|
json.get("authentication"), Authentication::class.java
|
||||||
|
) else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is JsonPrimitive -> {
|
||||||
|
try {
|
||||||
|
return MavenRepositoryConfig(url = URL(json.asString))
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
throw JsonParseException("Invalid URL: ${json.asString}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw JsonParseException("Unsupported Maven repository configuration type. (Only support JSON object or url string)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object UsernameAuthenticatorSerializer : JsonSerializer<UsernameAuthenticator>,
|
||||||
|
JsonDeserializer<UsernameAuthenticator> {
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
src: UsernameAuthenticator,
|
||||||
|
typeOfSrc: Type?,
|
||||||
|
context: JsonSerializationContext?
|
||||||
|
): JsonElement {
|
||||||
|
return src.toJsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type?,
|
||||||
|
context: JsonDeserializationContext?
|
||||||
|
): UsernameAuthenticator? {
|
||||||
|
if (json.isJsonNull) {
|
||||||
|
return null
|
||||||
|
} else if (!json.isJsonObject) {
|
||||||
|
throw JsonParseException("Invalid attribute value type.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonObj = json.asJsonObject
|
||||||
|
|
||||||
|
if (jsonObj["username"]?.isJsonPrimitive != true) {
|
||||||
|
throw JsonParseException("Invalid attribute value: username")
|
||||||
|
} else if (jsonObj["password"]?.isJsonPrimitive != true) {
|
||||||
|
throw JsonParseException("Invalid attribute value: password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonObj["username"].asString.isEmpty() || jsonObj["password"].asString.isEmpty()) {
|
||||||
|
throw JsonParseException("`username` or `password` is empty.")
|
||||||
|
}
|
||||||
|
return UsernameAuthenticator(jsonObj["username"].asString, jsonObj["password"].asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object ProxyConfigSerializer : JsonSerializer<ProxyConfig>, JsonDeserializer<ProxyConfig> {
|
||||||
|
override fun serialize(src: ProxyConfig?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||||
|
if (src == null) {
|
||||||
|
return JsonNull.INSTANCE
|
||||||
|
}
|
||||||
|
return JsonObject().apply {
|
||||||
|
addProperty("type", src.type.name)
|
||||||
|
addProperty("host", src.host)
|
||||||
|
addProperty("port", src.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): ProxyConfig {
|
||||||
|
if (json == null || json.isJsonNull) {
|
||||||
|
return ProxyConfig()
|
||||||
|
} else if (json !is JsonObject) {
|
||||||
|
throw JsonParseException("Invalid json type.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val typeStr = json["type"]?.asString ?: return ProxyConfig()
|
||||||
|
val type = try {
|
||||||
|
ProxyType.valueOf(typeStr)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw JsonParseException("Invalid proxy type: `$typeStr`")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.has("host") || !json.has("port")) {
|
||||||
|
throw JsonParseException("Missing `host` field or `port` field.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProxyConfig(
|
||||||
|
type = type,
|
||||||
|
host = json["host"].asString,
|
||||||
|
port = json["port"].asInt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object BotConfigSerializer : JsonSerializer<BotConfig>, JsonDeserializer<BotConfig> {
|
||||||
|
|
||||||
|
private val defaultConfig = BotConfig(account = BotAccount("__Default__", "__Default__", 0))
|
||||||
|
|
||||||
|
override fun serialize(src: BotConfig, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||||
|
return JsonObject().apply {
|
||||||
|
addProperty("enabled", src.enabled)
|
||||||
|
add("account", context.serialize(src.account))
|
||||||
|
addProperty("disableBuiltInAbility", src.disableBuiltInAbility)
|
||||||
|
addProperty("autoUpdateCommandList", src.autoUpdateCommandList)
|
||||||
|
add("extensions", context.serialize(src.extensions))
|
||||||
|
add("proxy", ProxyConfigSerializer.serialize(src.proxy, ProxyConfig::class.java, context))
|
||||||
|
addProperty("baseApiUrl", src.baseApiUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): BotConfig {
|
||||||
|
if (json !is JsonObject) {
|
||||||
|
throw JsonParseException("Unsupported JSON type.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.has("account")) {
|
||||||
|
throw JsonParseException("Missing `account` field.")
|
||||||
|
} else if (!json.get("account").isJsonObject) {
|
||||||
|
throw JsonParseException("Invalid `account` field type.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 json 反序列化 BotConfig(使用构造函数)
|
||||||
|
return BotConfig(
|
||||||
|
enabled = json.get("enabled")?.asBoolean ?: defaultConfig.enabled,
|
||||||
|
account = context.deserialize(json.get("account"), BotAccount::class.java)!!,
|
||||||
|
disableBuiltInAbility = json.get("disableBuiltInAbility")?.asBoolean ?: defaultConfig.disableBuiltInAbility,
|
||||||
|
autoUpdateCommandList = json.get("autoUpdateCommandList")?.asBoolean ?: defaultConfig.autoUpdateCommandList,
|
||||||
|
extensions = context.deserialize(json.get("extensions"), object : TypeToken<Set<Artifact>>() {}.type)
|
||||||
|
?: defaultConfig.extensions,
|
||||||
|
proxy = context.deserialize(json.get("proxy"), ProxyConfig::class.java) ?: defaultConfig.proxy,
|
||||||
|
baseApiUrl = json.get("baseApiUrl")?.asString ?: defaultConfig.baseApiUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BotAccountSerializer : JsonDeserializer<BotAccount> {
|
||||||
|
|
||||||
|
private val tokenCheckRegex = Pattern.compile("\\d+:[a-zA-Z\\d_-]{35}")
|
||||||
|
|
||||||
|
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): BotAccount {
|
||||||
|
if (json == null || json.isJsonNull) {
|
||||||
|
throw JsonParseException("Missing `account` field.")
|
||||||
|
} else if (!json.isJsonObject) {
|
||||||
|
throw JsonParseException("Invalid `account` field type.")
|
||||||
|
}
|
||||||
|
val jsonObj = json.asJsonObject
|
||||||
|
|
||||||
|
val name = jsonObj.getPrimitiveValueOrThrow("name").asString
|
||||||
|
val token = jsonObj.getPrimitiveValueOrThrow("token").asString.let {
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
throw JsonParseException("`token` cannot be empty.")
|
||||||
|
} else if (!tokenCheckRegex.matcher(it).matches()) {
|
||||||
|
throw JsonParseException("`token` is invalid.")
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val creatorId = try {
|
||||||
|
jsonObj.getPrimitiveValueOrThrow("creatorId").asLong
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw JsonParseException("`creatorId` must be a number.")
|
||||||
|
}.apply {
|
||||||
|
if (this < 0) {
|
||||||
|
throw JsonParseException("`creatorId` must be a positive number.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BotAccount(name, token, creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
522
scalabot-meta/src/test/kotlin/ConfigsTest.kt
Normal file
522
scalabot-meta/src/test/kotlin/ConfigsTest.kt
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import net.lamgc.scalabot.config.serializer.*
|
||||||
|
import org.eclipse.aether.artifact.Artifact
|
||||||
|
import org.eclipse.aether.artifact.DefaultArtifact
|
||||||
|
import org.eclipse.aether.repository.Authentication
|
||||||
|
import org.eclipse.aether.repository.AuthenticationContext
|
||||||
|
import org.eclipse.aether.repository.Proxy
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
internal class BotAccountTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `id getter`() {
|
||||||
|
val accountId = abs(Random().nextInt()).toLong()
|
||||||
|
Assertions.assertEquals(accountId, BotAccount("Test", "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", 0).id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deserializerTest() {
|
||||||
|
val accountId = abs(Random().nextInt()).toLong()
|
||||||
|
val creatorId = abs(Random().nextInt()).toLong()
|
||||||
|
val botAccountJsonObject = gson.fromJson(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"name": "TestBot",
|
||||||
|
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||||
|
"creatorId": $creatorId
|
||||||
|
}
|
||||||
|
""".trimIndent(), JsonObject::class.java
|
||||||
|
)
|
||||||
|
val botAccount = Gson().fromJson(botAccountJsonObject, BotAccount::class.java)
|
||||||
|
assertEquals(accountId, botAccount.id)
|
||||||
|
assertEquals("TestBot", botAccount.name)
|
||||||
|
assertEquals(creatorId, botAccount.creatorId)
|
||||||
|
assertEquals("${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", botAccount.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun serializerTest() {
|
||||||
|
val accountId = abs(Random().nextInt()).toLong()
|
||||||
|
val creatorId = abs(Random().nextInt()).toLong()
|
||||||
|
val botAccount = BotAccount("TestBot", "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", creatorId)
|
||||||
|
val botAccountJsonObject = gson.toJsonTree(botAccount)
|
||||||
|
assertTrue(botAccountJsonObject is JsonObject)
|
||||||
|
assertEquals(botAccount.name, botAccountJsonObject["name"].asString)
|
||||||
|
assertEquals(botAccount.token, botAccountJsonObject["token"].asString)
|
||||||
|
assertNull(botAccountJsonObject["id"])
|
||||||
|
Assertions.assertEquals(creatorId, botAccountJsonObject["creatorId"].asLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BotConfigTest {
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||||
|
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||||
|
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json serialize`() {
|
||||||
|
val minimumExpectConfig = BotConfig(
|
||||||
|
account = BotAccount(
|
||||||
|
name = "TestBot",
|
||||||
|
token = "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||||
|
creatorId = 123456789L
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = gson.toJsonTree(minimumExpectConfig)
|
||||||
|
assertTrue(json is JsonObject)
|
||||||
|
assertEquals(minimumExpectConfig.enabled, json.get("enabled").asBoolean)
|
||||||
|
|
||||||
|
assertEquals(minimumExpectConfig.account.name, json.get("account").asJsonObject.get("name").asString)
|
||||||
|
assertEquals(minimumExpectConfig.account.token, json.get("account").asJsonObject.get("token").asString)
|
||||||
|
assertEquals(minimumExpectConfig.account.creatorId, json.get("account").asJsonObject.get("creatorId").asLong)
|
||||||
|
assertNull(json.get("account").asJsonObject.get("id"))
|
||||||
|
|
||||||
|
assertEquals(minimumExpectConfig.proxy.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||||
|
assertEquals(minimumExpectConfig.proxy.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||||
|
assertEquals(minimumExpectConfig.proxy.type.name, json.get("proxy").asJsonObject.get("type").asString)
|
||||||
|
|
||||||
|
assertEquals(minimumExpectConfig.disableBuiltInAbility, json.get("disableBuiltInAbility").asBoolean)
|
||||||
|
assertEquals(minimumExpectConfig.autoUpdateCommandList, json.get("autoUpdateCommandList").asBoolean)
|
||||||
|
|
||||||
|
assertNotNull(json.get("extensions"))
|
||||||
|
assertTrue(json.get("extensions").isJsonArray)
|
||||||
|
assertTrue(json.get("extensions").asJsonArray.isEmpty)
|
||||||
|
|
||||||
|
assertEquals(minimumExpectConfig.baseApiUrl, json.get("baseApiUrl").asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserialize`() {
|
||||||
|
val expectExtensionArtifact = DefaultArtifact("org.example.test:test-extension:1.0.0")
|
||||||
|
@Language("JSON5") val looksGoodJson = """
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"account": {
|
||||||
|
"name": "TestBot",
|
||||||
|
"token": "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||||
|
"creatorId": 123456789
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080,
|
||||||
|
"type": "HTTP"
|
||||||
|
},
|
||||||
|
"disableBuiltInAbility": false,
|
||||||
|
"autoUpdateCommandList": true,
|
||||||
|
"extensions": [
|
||||||
|
"$expectExtensionArtifact"
|
||||||
|
],
|
||||||
|
"baseApiUrl": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val actualConfig = gson.fromJson(looksGoodJson, BotConfig::class.java)
|
||||||
|
|
||||||
|
assertEquals(false, actualConfig.enabled)
|
||||||
|
|
||||||
|
assertEquals("TestBot", actualConfig.account.name)
|
||||||
|
assertEquals("123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", actualConfig.account.token)
|
||||||
|
assertEquals(123456789L, actualConfig.account.creatorId)
|
||||||
|
|
||||||
|
assertEquals("localhost", actualConfig.proxy.host)
|
||||||
|
assertEquals(8080, actualConfig.proxy.port)
|
||||||
|
assertEquals(ProxyType.HTTP, actualConfig.proxy.type)
|
||||||
|
|
||||||
|
assertEquals(false, actualConfig.disableBuiltInAbility)
|
||||||
|
assertEquals(true, actualConfig.autoUpdateCommandList)
|
||||||
|
|
||||||
|
assertEquals(1, actualConfig.extensions.size)
|
||||||
|
assertEquals(expectExtensionArtifact, actualConfig.extensions.first())
|
||||||
|
|
||||||
|
assertEquals("http://localhost:8080", actualConfig.baseApiUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserialize - minimum parameters`() {
|
||||||
|
@Language("JSON5") val minimumLooksGoodJson = """
|
||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"name": "TestBot",
|
||||||
|
"token": "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||||
|
"creatorId": 123456789
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val expectDefaultConfig = BotConfig(account = BotAccount("Test", "Test", 0))
|
||||||
|
val actualMinimumConfig = gson.fromJson(minimumLooksGoodJson, BotConfig::class.java)
|
||||||
|
assertNotNull(actualMinimumConfig)
|
||||||
|
assertEquals("TestBot", actualMinimumConfig.account.name)
|
||||||
|
assertEquals("123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", actualMinimumConfig.account.token)
|
||||||
|
assertEquals(123456789, actualMinimumConfig.account.creatorId)
|
||||||
|
|
||||||
|
assertEquals(expectDefaultConfig.enabled, actualMinimumConfig.enabled)
|
||||||
|
assertEquals(expectDefaultConfig.disableBuiltInAbility, actualMinimumConfig.disableBuiltInAbility)
|
||||||
|
assertEquals(expectDefaultConfig.autoUpdateCommandList, actualMinimumConfig.autoUpdateCommandList)
|
||||||
|
assertEquals(expectDefaultConfig.proxy, actualMinimumConfig.proxy)
|
||||||
|
assertEquals(expectDefaultConfig.baseApiUrl, actualMinimumConfig.baseApiUrl)
|
||||||
|
|
||||||
|
assertTrue(expectDefaultConfig.extensions.containsAll(actualMinimumConfig.extensions))
|
||||||
|
assertTrue(actualMinimumConfig.extensions.containsAll(expectDefaultConfig.extensions))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ProxyConfigTest {
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||||
|
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json serialize`() {
|
||||||
|
val proxyConfig = ProxyConfig(
|
||||||
|
host = "localhost",
|
||||||
|
port = 8080,
|
||||||
|
type = ProxyType.HTTP
|
||||||
|
)
|
||||||
|
val json = gson.toJsonTree(proxyConfig)
|
||||||
|
assertTrue(json is JsonObject)
|
||||||
|
assertEquals(proxyConfig.host, json.get("host").asString)
|
||||||
|
assertEquals(proxyConfig.port, json.get("port").asInt)
|
||||||
|
assertEquals(proxyConfig.type.name, json.get("type").asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserialize`() {
|
||||||
|
@Language("JSON5") val looksGoodJson = """
|
||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080,
|
||||||
|
"type": "HTTP"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val actualConfig = gson.fromJson(looksGoodJson, ProxyConfig::class.java)
|
||||||
|
|
||||||
|
assertEquals("localhost", actualConfig.host)
|
||||||
|
assertEquals(8080, actualConfig.port)
|
||||||
|
assertEquals(ProxyType.HTTP, actualConfig.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toString test`() {
|
||||||
|
assertEquals("NO_PROXY", ProxyConfig(ProxyType.NO_PROXY).toString())
|
||||||
|
assertEquals("HTTP://example.org:1008", ProxyConfig(ProxyType.HTTP, "example.org", 1008).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MetricsConfigTest {
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json serializer`() {
|
||||||
|
val config = MetricsConfig(
|
||||||
|
enable = true,
|
||||||
|
port = 8800,
|
||||||
|
bindAddress = "127.0.0.1",
|
||||||
|
authenticator = UsernameAuthenticator(
|
||||||
|
username = "username",
|
||||||
|
password = "password"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = gson.toJsonTree(config).asJsonObject
|
||||||
|
|
||||||
|
assertEquals(config.enable, json.get("enable").asBoolean)
|
||||||
|
assertEquals(config.port, json.get("port").asInt)
|
||||||
|
assertEquals(config.bindAddress, json.get("bindAddress").asString)
|
||||||
|
assertNotNull(config.authenticator)
|
||||||
|
assertTrue(
|
||||||
|
config.authenticator!!.checkCredentials(
|
||||||
|
json.get("authenticator").asJsonObject.get("username").asString,
|
||||||
|
json.get("authenticator").asJsonObject.get("password").asString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val expectDefaultValueConfig = MetricsConfig()
|
||||||
|
val defaultValueJson = gson.toJsonTree(expectDefaultValueConfig).asJsonObject
|
||||||
|
assertEquals(expectDefaultValueConfig.enable, defaultValueJson.get("enable").asBoolean)
|
||||||
|
assertEquals(expectDefaultValueConfig.port, defaultValueJson.get("port").asInt)
|
||||||
|
assertEquals(expectDefaultValueConfig.bindAddress, defaultValueJson.get("bindAddress").asString)
|
||||||
|
assertNull(defaultValueJson.get("authenticator"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserializer`() {
|
||||||
|
val json = """
|
||||||
|
{
|
||||||
|
"enable": true,
|
||||||
|
"port": 8800,
|
||||||
|
"bindAddress": "127.0.0.1",
|
||||||
|
"authenticator": {
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val config = gson.fromJson(json, MetricsConfig::class.java)
|
||||||
|
assertEquals(true, config.enable)
|
||||||
|
assertEquals(8800, config.port)
|
||||||
|
assertEquals("127.0.0.1", config.bindAddress)
|
||||||
|
assertNotNull(config.authenticator)
|
||||||
|
assertTrue(config.authenticator!!.checkCredentials("username", "password"))
|
||||||
|
|
||||||
|
val defaultValueConfig = MetricsConfig()
|
||||||
|
val defaultValueJson = gson.toJsonTree(defaultValueConfig).asJsonObject
|
||||||
|
assertEquals(defaultValueConfig.enable, defaultValueJson.get("enable").asBoolean)
|
||||||
|
assertEquals(defaultValueConfig.port, defaultValueJson.get("port").asInt)
|
||||||
|
assertEquals(defaultValueConfig.bindAddress, defaultValueJson.get("bindAddress").asString)
|
||||||
|
assertNull(defaultValueJson.get("authenticator"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserializer - default value`() {
|
||||||
|
val actualConfig = gson.fromJson("{}", MetricsConfig::class.java)
|
||||||
|
val expectConfig = MetricsConfig()
|
||||||
|
|
||||||
|
assertEquals(expectConfig, actualConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MavenRepositoryConfigTest {
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||||
|
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json serializer`() {
|
||||||
|
val config = MavenRepositoryConfig(
|
||||||
|
id = "test",
|
||||||
|
url = URL("http://localhost:8080/repository"),
|
||||||
|
proxy = Proxy(
|
||||||
|
"http",
|
||||||
|
"localhost",
|
||||||
|
8080,
|
||||||
|
),
|
||||||
|
layout = "legacy",
|
||||||
|
enableReleases = false,
|
||||||
|
enableSnapshots = true,
|
||||||
|
authentication = null
|
||||||
|
)
|
||||||
|
val json = gson.toJsonTree(config).asJsonObject
|
||||||
|
assertEquals(config.id, json.get("id").asString)
|
||||||
|
assertEquals(config.url.toString(), json.get("url").asString)
|
||||||
|
|
||||||
|
assertEquals(config.proxy!!.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||||
|
assertEquals(config.proxy!!.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||||
|
assertEquals(config.proxy!!.type, json.get("proxy").asJsonObject.get("type").asString)
|
||||||
|
|
||||||
|
assertEquals(config.layout, json.get("layout").asString)
|
||||||
|
assertEquals(config.enableReleases, json.get("enableReleases").asBoolean)
|
||||||
|
assertEquals(config.enableSnapshots, json.get("enableSnapshots").asBoolean)
|
||||||
|
assertNull(json.get("authentication"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserializer`() {
|
||||||
|
@Language("JSON5")
|
||||||
|
val json = """
|
||||||
|
{
|
||||||
|
"id": "test",
|
||||||
|
"url": "http://localhost:8080/repository",
|
||||||
|
"proxy": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080,
|
||||||
|
"type": "HTTP"
|
||||||
|
},
|
||||||
|
"layout": "legacy",
|
||||||
|
"enableReleases": false,
|
||||||
|
"enableSnapshots": true,
|
||||||
|
"authentication": {
|
||||||
|
"username": "testUser",
|
||||||
|
"password": "testPassword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val config = gson.fromJson(json, MavenRepositoryConfig::class.java)
|
||||||
|
|
||||||
|
assertEquals("test", config.id)
|
||||||
|
assertEquals(URL("http://localhost:8080/repository"), config.url)
|
||||||
|
assertEquals(
|
||||||
|
Proxy(
|
||||||
|
"HTTP",
|
||||||
|
"localhost",
|
||||||
|
8080
|
||||||
|
), config.proxy
|
||||||
|
)
|
||||||
|
assertEquals("legacy", config.layout)
|
||||||
|
assertEquals(false, config.enableReleases)
|
||||||
|
assertEquals(true, config.enableSnapshots)
|
||||||
|
assertNotNull(config.authentication)
|
||||||
|
|
||||||
|
val authContext = mockk<AuthenticationContext> {
|
||||||
|
every { put(ofType(String::class), any()) } answers { }
|
||||||
|
}
|
||||||
|
config.authentication!!.fill(authContext, null, emptyMap())
|
||||||
|
|
||||||
|
verify {
|
||||||
|
authContext.put(any(), "testUser")
|
||||||
|
authContext.put(any(), "testPassword".toCharArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AppConfigTest {
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||||
|
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||||
|
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||||
|
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json serializer - default value`() {
|
||||||
|
val config = AppConfig()
|
||||||
|
val json = gson.toJsonTree(config).asJsonObject
|
||||||
|
assertEquals(config.proxy.type.name, json.get("proxy").asJsonObject.get("type").asString)
|
||||||
|
assertEquals(config.proxy.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||||
|
assertEquals(config.proxy.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||||
|
assertEquals(config.metrics.enable, json.get("metrics").asJsonObject.get("enable").asBoolean)
|
||||||
|
assertEquals(config.metrics.port, json.get("metrics").asJsonObject.get("port").asInt)
|
||||||
|
assertEquals(config.metrics.bindAddress, json.get("metrics").asJsonObject.get("bindAddress").asString)
|
||||||
|
assertNull(json["metrics"].asJsonObject.get("authenticator"))
|
||||||
|
assertTrue(json["mavenRepositories"].asJsonArray.isEmpty)
|
||||||
|
assertNull(json["mavenLocalRepository"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json serializer - Provide values`() {
|
||||||
|
val config = AppConfig(
|
||||||
|
proxy = ProxyConfig(
|
||||||
|
type = ProxyType.HTTP,
|
||||||
|
host = "localhost",
|
||||||
|
port = 8080
|
||||||
|
),
|
||||||
|
metrics = MetricsConfig(
|
||||||
|
enable = true,
|
||||||
|
port = 8800,
|
||||||
|
bindAddress = "127.0.0.1",
|
||||||
|
authenticator = UsernameAuthenticator(
|
||||||
|
username = "username",
|
||||||
|
password = "password"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
mavenRepositories = listOf(
|
||||||
|
MavenRepositoryConfig(
|
||||||
|
url = URL("https://repository.maven.apache.org/maven2/")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
mavenLocalRepository = "file:///tmp/maven-local-repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = gson.toJsonTree(config).asJsonObject
|
||||||
|
|
||||||
|
assertEquals(config.proxy.type.name, json.get("proxy").asJsonObject.get("type").asString)
|
||||||
|
assertEquals(config.proxy.host, json.get("proxy").asJsonObject.get("host").asString)
|
||||||
|
assertEquals(config.proxy.port, json.get("proxy").asJsonObject.get("port").asInt)
|
||||||
|
|
||||||
|
assertEquals(config.metrics.enable, json.get("metrics").asJsonObject.get("enable").asBoolean)
|
||||||
|
assertEquals(config.metrics.port, json.get("metrics").asJsonObject.get("port").asInt)
|
||||||
|
assertEquals(config.metrics.bindAddress, json.get("metrics").asJsonObject.get("bindAddress").asString)
|
||||||
|
assertNotNull(config.metrics.authenticator)
|
||||||
|
assertTrue(
|
||||||
|
config.metrics.authenticator!!.checkCredentials(
|
||||||
|
json.get("metrics").asJsonObject.get("authenticator").asJsonObject.get("username").asString,
|
||||||
|
json.get("metrics").asJsonObject.get("authenticator").asJsonObject.get("password").asString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1, json["mavenRepositories"].asJsonArray.size())
|
||||||
|
assertEquals(
|
||||||
|
config.mavenRepositories[0].url.toString(),
|
||||||
|
json["mavenRepositories"].asJsonArray[0].asJsonObject.get("url").asString
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(config.mavenLocalRepository, json["mavenLocalRepository"].asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserializer - complete`() {
|
||||||
|
val json = """
|
||||||
|
{
|
||||||
|
"proxy": {
|
||||||
|
"type": "HTTP",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"enable": true,
|
||||||
|
"port": 8800,
|
||||||
|
"bindAddress": "127.0.0.1",
|
||||||
|
"authenticator": {
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mavenRepositories": [
|
||||||
|
{
|
||||||
|
"url": "https://repository.maven.apache.org/maven2/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mavenLocalRepository": "file:///tmp/maven-local-repository"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val config = gson.fromJson(json, AppConfig::class.java)
|
||||||
|
|
||||||
|
assertEquals(ProxyType.HTTP, config.proxy.type)
|
||||||
|
assertEquals("localhost", config.proxy.host)
|
||||||
|
assertEquals(8080, config.proxy.port)
|
||||||
|
|
||||||
|
assertEquals(true, config.metrics.enable)
|
||||||
|
assertEquals(8800, config.metrics.port)
|
||||||
|
assertEquals("127.0.0.1", config.metrics.bindAddress)
|
||||||
|
assertNotNull(config.metrics.authenticator)
|
||||||
|
assertTrue(config.metrics.authenticator!!.checkCredentials("username", "password"))
|
||||||
|
|
||||||
|
assertEquals(1, config.mavenRepositories.size)
|
||||||
|
assertEquals(URL("https://repository.maven.apache.org/maven2/"), config.mavenRepositories[0].url)
|
||||||
|
|
||||||
|
assertEquals("file:///tmp/maven-local-repository", config.mavenLocalRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserializer - default value`() {
|
||||||
|
val actualConfig = gson.fromJson("{}", AppConfig::class.java)
|
||||||
|
val expectConfig = AppConfig()
|
||||||
|
|
||||||
|
assertEquals(expectConfig, actualConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
scalabot-meta/src/test/kotlin/UsernameAuthenticatorTest.kt
Normal file
38
scalabot-meta/src/test/kotlin/UsernameAuthenticatorTest.kt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
internal class UsernameAuthenticatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkCredentialsTest() {
|
||||||
|
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||||
|
assertTrue(authenticator.checkCredentials("testUser", "testPassword"))
|
||||||
|
assertFalse(authenticator.checkCredentials("falseUser", "testPassword"))
|
||||||
|
assertFalse(authenticator.checkCredentials("testUser", "falsePassword"))
|
||||||
|
assertFalse(authenticator.checkCredentials("falseUser", "falsePassword"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun toJsonObjectTest() {
|
||||||
|
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||||
|
val jsonObject = authenticator.toJsonObject()
|
||||||
|
assertEquals("testUser", jsonObject["username"]?.asString)
|
||||||
|
assertEquals("testPassword", jsonObject["password"]?.asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun equalsTest() {
|
||||||
|
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||||
|
assertEquals(authenticator, UsernameAuthenticator("testUser", "testPassword"))
|
||||||
|
assertEquals(authenticator.hashCode(), UsernameAuthenticator("testUser", "testPassword").hashCode())
|
||||||
|
assertNotEquals(authenticator, UsernameAuthenticator("testUser", "falsePassword"))
|
||||||
|
assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("testUser", "falsePassword").hashCode())
|
||||||
|
assertNotEquals(authenticator, UsernameAuthenticator("falseUser", "testPassword"))
|
||||||
|
assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("falseUser", "testPassword").hashCode())
|
||||||
|
assertNotEquals(authenticator, UsernameAuthenticator("falseUser", "falsePassword"))
|
||||||
|
assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("falseUser", "falsePassword").hashCode())
|
||||||
|
assertFalse(authenticator.equals(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
852
scalabot-meta/src/test/kotlin/serializer/SerializeUtilsTest.kt
Normal file
852
scalabot-meta/src/test/kotlin/serializer/SerializeUtilsTest.kt
Normal file
@ -0,0 +1,852 @@
|
|||||||
|
package net.lamgc.scalabot.config.serializer
|
||||||
|
|
||||||
|
import com.google.gson.*
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import net.lamgc.scalabot.config.*
|
||||||
|
import net.lamgc.scalabot.config.serializer.SerializeUtils.getPrimitiveValueOrThrow
|
||||||
|
import org.eclipse.aether.artifact.Artifact
|
||||||
|
import org.eclipse.aether.artifact.DefaultArtifact
|
||||||
|
import org.eclipse.aether.repository.Authentication
|
||||||
|
import org.eclipse.aether.repository.AuthenticationContext
|
||||||
|
import org.eclipse.aether.repository.Proxy
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.net.URL
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
internal class SerializeUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getPrimitiveValueOrThrow test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
JsonObject().getPrimitiveValueOrThrow("NOT_EXIST_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
JsonObject().apply {
|
||||||
|
add("testKey", JsonArray())
|
||||||
|
}.getPrimitiveValueOrThrow("testKey")
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
JsonObject().apply {
|
||||||
|
add("testKey", JsonObject())
|
||||||
|
}.getPrimitiveValueOrThrow("testKey")
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
JsonObject().apply {
|
||||||
|
add("testKey", JsonNull.INSTANCE)
|
||||||
|
}.getPrimitiveValueOrThrow("testKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectKey = "STRING_KEY"
|
||||||
|
val expectValue = JsonPrimitive("A STRING")
|
||||||
|
assertEquals(expectValue, JsonObject()
|
||||||
|
.apply { add(expectKey, expectValue) }
|
||||||
|
.getPrimitiveValueOrThrow(expectKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ProxyTypeSerializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `serialize test`() {
|
||||||
|
for (type in ProxyType.entries) {
|
||||||
|
assertEquals(
|
||||||
|
JsonPrimitive(type.name), ProxyTypeSerializer.serialize(type, null, null),
|
||||||
|
"ProxyType 序列化结果与预期不符."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deserialize test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyTypeSerializer.deserialize(JsonObject(), null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyTypeSerializer.deserialize(JsonArray(), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyTypeSerializer.deserialize(JsonPrimitive("NOT_IN_ENUM_VALUE"), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ProxyType.NO_PROXY,
|
||||||
|
ProxyTypeSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (type in ProxyType.entries) {
|
||||||
|
assertEquals(
|
||||||
|
type, ProxyTypeSerializer.deserialize(JsonPrimitive(type.name), null, null),
|
||||||
|
"ProxyType 反序列化结果与预期不符."
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
type, ProxyTypeSerializer.deserialize(JsonPrimitive(" ${type.name} "), null, null),
|
||||||
|
"ProxyType 反序列化时未对 Json 字符串进行修剪(trim)."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MavenRepositoryConfigSerializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unsupported json type deserialize test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
JsonArray(),
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
JsonNull.INSTANCE,
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json primitive deserialize test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
JsonPrimitive("NOT A URL."),
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectRepoUrl = "https://repo.example.org/maven"
|
||||||
|
val config = MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
JsonPrimitive(expectRepoUrl),
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(config.id)
|
||||||
|
assertEquals(URL(expectRepoUrl), config.url)
|
||||||
|
assertNull(config.proxy, "Proxy 默认值不为 null.")
|
||||||
|
assertEquals("default", config.layout)
|
||||||
|
assertTrue(config.enableReleases)
|
||||||
|
assertTrue(config.enableSnapshots)
|
||||||
|
assertNull(config.authentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json object default deserialize test`() {
|
||||||
|
val expectRepoUrl = "https://repo.example.org/maven"
|
||||||
|
val jsonObject = JsonObject()
|
||||||
|
jsonObject.addProperty("url", expectRepoUrl)
|
||||||
|
val config = MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
jsonObject,
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNull(config.id)
|
||||||
|
assertEquals(URL(expectRepoUrl), config.url)
|
||||||
|
assertNull(config.proxy, "Proxy 默认值不为 null.")
|
||||||
|
assertEquals("default", config.layout)
|
||||||
|
assertTrue(config.enableReleases)
|
||||||
|
assertTrue(config.enableSnapshots)
|
||||||
|
assertNull(config.authentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json object deserialize test`() {
|
||||||
|
@Language("JSON5")
|
||||||
|
val looksGoodJsonString = """
|
||||||
|
{
|
||||||
|
"id": "test-repository",
|
||||||
|
"url": "https://repo.example.org/maven",
|
||||||
|
"proxy": {
|
||||||
|
"type": "http",
|
||||||
|
"host": "127.0.1.1",
|
||||||
|
"port": 10800
|
||||||
|
},
|
||||||
|
"layout": "default",
|
||||||
|
"enableReleases": false,
|
||||||
|
"enableSnapshots": true
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val jsonObject = Gson().fromJson(looksGoodJsonString, JsonObject::class.java)
|
||||||
|
var config = MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
jsonObject,
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(jsonObject["id"].asString, config.id)
|
||||||
|
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||||
|
assertEquals(Proxy("http", "127.0.1.1", 10800), config.proxy)
|
||||||
|
assertEquals(jsonObject["layout"].asString, config.layout)
|
||||||
|
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||||
|
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
|
jsonObject.add("proxy", JsonNull.INSTANCE)
|
||||||
|
jsonObject.remove("layout")
|
||||||
|
|
||||||
|
config = MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
jsonObject,
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(jsonObject["id"].asString, config.id)
|
||||||
|
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||||
|
assertNull(config.proxy)
|
||||||
|
assertEquals("default", config.layout)
|
||||||
|
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||||
|
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
|
jsonObject.add("layout", mockk<JsonPrimitive> {
|
||||||
|
every { asString }.returns(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
config = MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
jsonObject,
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(jsonObject["id"].asString, config.id)
|
||||||
|
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||||
|
assertNull(config.proxy)
|
||||||
|
assertEquals("default", config.layout)
|
||||||
|
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||||
|
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||||
|
assertNull(config.authentication)
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
|
jsonObject.add("authentication", JsonObject().apply {
|
||||||
|
addProperty("username", "testUsername")
|
||||||
|
addProperty("password", "testPassword")
|
||||||
|
})
|
||||||
|
|
||||||
|
config = MavenRepositoryConfigSerializer.deserialize(
|
||||||
|
jsonObject,
|
||||||
|
MavenRepositoryConfig::class.java,
|
||||||
|
TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(jsonObject["id"].asString, config.id)
|
||||||
|
assertEquals(URL(jsonObject["url"].asString), config.url)
|
||||||
|
assertNull(config.proxy)
|
||||||
|
assertEquals("default", config.layout)
|
||||||
|
assertEquals(jsonObject["enableReleases"].asBoolean, config.enableReleases)
|
||||||
|
assertEquals(jsonObject["enableSnapshots"].asBoolean, config.enableSnapshots)
|
||||||
|
assertNotNull(config.authentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestJsonSerializationContext(private val gson: Gson) : JsonDeserializationContext,
|
||||||
|
JsonSerializationContext {
|
||||||
|
|
||||||
|
override fun <T : Any?> deserialize(json: JsonElement?, typeOfT: Type): T {
|
||||||
|
return gson.fromJson(json, typeOfT)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun default(): TestJsonSerializationContext {
|
||||||
|
return TestJsonSerializationContext(
|
||||||
|
GsonBuilder()
|
||||||
|
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
|
||||||
|
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||||
|
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||||
|
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||||
|
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
|
||||||
|
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
|
||||||
|
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||||
|
.create()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(src: Any?): JsonElement {
|
||||||
|
return gson.toJsonTree(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(src: Any?, typeOfSrc: Type?): JsonElement {
|
||||||
|
return gson.toJsonTree(src, typeOfSrc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AuthenticationSerializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deserialize test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
AuthenticationSerializer.deserialize(
|
||||||
|
JsonNull.INSTANCE,
|
||||||
|
Authentication::class.java, TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
AuthenticationSerializer.deserialize(
|
||||||
|
JsonArray(),
|
||||||
|
Authentication::class.java, TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
AuthenticationSerializer.deserialize(
|
||||||
|
JsonPrimitive("A STRING"),
|
||||||
|
Authentication::class.java, TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectJsonObject = JsonObject().apply {
|
||||||
|
addProperty("username", "testUsername")
|
||||||
|
addProperty("password", "testPassword")
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockContext = mockk<AuthenticationContext> {
|
||||||
|
every { put(any(), any()) }.answers { }
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = AuthenticationSerializer.deserialize(
|
||||||
|
expectJsonObject,
|
||||||
|
Authentication::class.java, TestJsonSerializationContext.default()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertNotNull(result)
|
||||||
|
result.fill(mockContext, "username", null)
|
||||||
|
result.fill(mockContext, "password", null)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
mockContext.put("username", "testUsername")
|
||||||
|
mockContext.put("password", "testPassword".toCharArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BotConfigSerializerTest {
|
||||||
|
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
|
||||||
|
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
|
||||||
|
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
|
||||||
|
.registerTypeAdapter(ProxyConfig::class.java, ProxyConfigSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `serializer test`() {
|
||||||
|
// 检查 BotConfig 的序列化
|
||||||
|
val botConfig = BotConfig(
|
||||||
|
account = BotAccount(
|
||||||
|
name = "test-bot",
|
||||||
|
token = "test-token",
|
||||||
|
creatorId = 10000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 使用 gson 序列化 botConfig, 并检查序列化结果
|
||||||
|
val jsonObject = gson.toJsonTree(botConfig) as JsonObject
|
||||||
|
assertEquals("test-bot", jsonObject["account"].asJsonObject["name"].asString)
|
||||||
|
assertEquals("test-token", jsonObject["account"].asJsonObject["token"].asString)
|
||||||
|
assertEquals(10000, jsonObject["account"].asJsonObject["creatorId"].asInt)
|
||||||
|
|
||||||
|
assertEquals(botConfig.enabled, jsonObject["enabled"].asBoolean)
|
||||||
|
assertEquals(botConfig.proxy.host, jsonObject["proxy"].asJsonObject["host"].asString)
|
||||||
|
assertEquals(botConfig.proxy.port, jsonObject["proxy"].asJsonObject["port"].asInt)
|
||||||
|
assertEquals(botConfig.proxy.type.name, jsonObject["proxy"].asJsonObject["type"].asString)
|
||||||
|
assertEquals(botConfig.disableBuiltInAbility, jsonObject["disableBuiltInAbility"].asBoolean)
|
||||||
|
assertEquals(botConfig.autoUpdateCommandList, jsonObject["autoUpdateCommandList"].asBoolean)
|
||||||
|
assertEquals(botConfig.extensions.isEmpty(), jsonObject["extensions"].asJsonArray.isEmpty)
|
||||||
|
assertEquals(botConfig.baseApiUrl, jsonObject["baseApiUrl"].asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deserialize test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonNull.INSTANCE,
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonArray(),
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonPrimitive("A STRING"),
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 BotConfig 的反序列化中是否能正确判断 account 的类型
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonObject().apply {
|
||||||
|
addProperty("account", "A STRING")
|
||||||
|
},
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonObject().apply {
|
||||||
|
add("account", JsonNull.INSTANCE)
|
||||||
|
},
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonObject().apply {
|
||||||
|
add("account", JsonArray())
|
||||||
|
},
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotConfigSerializer.deserialize(
|
||||||
|
JsonObject(),
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val expectBotAccount = BotAccount(
|
||||||
|
name = "test-bot",
|
||||||
|
token = "test-token",
|
||||||
|
creatorId = 10000
|
||||||
|
)
|
||||||
|
val expectDefaultBotConfig = BotConfig(account = expectBotAccount)
|
||||||
|
val minimumJsonObject = JsonObject().apply {
|
||||||
|
add("account", gson.toJsonTree(expectBotAccount))
|
||||||
|
}
|
||||||
|
val actualMinimumBotConfig = BotConfigSerializer.deserialize(
|
||||||
|
minimumJsonObject, BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
assertNotNull(actualMinimumBotConfig)
|
||||||
|
assertEquals(expectDefaultBotConfig, actualMinimumBotConfig)
|
||||||
|
|
||||||
|
val expectDefaultProxy = ProxyConfig(
|
||||||
|
type = ProxyType.HTTP,
|
||||||
|
host = "https://example.com",
|
||||||
|
port = 443
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
val jsonObject = JsonObject().apply {
|
||||||
|
add(
|
||||||
|
"account", gson.toJsonTree(
|
||||||
|
BotAccount(
|
||||||
|
name = "test-bot",
|
||||||
|
token = "test-token",
|
||||||
|
creatorId = 10000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addProperty("enabled", true)
|
||||||
|
add("proxy", gson.toJsonTree(expectDefaultProxy))
|
||||||
|
addProperty("disableBuiltInAbility", true)
|
||||||
|
addProperty("autoUpdateCommandList", true)
|
||||||
|
addProperty("baseApiUrl", "https://test.com")
|
||||||
|
add("extensions", JsonArray().apply {
|
||||||
|
add("org.example:test:1.0.0-SNAPSHOT")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val botConfig = BotConfigSerializer.deserialize(
|
||||||
|
jsonObject,
|
||||||
|
BotConfig::class.java, TestJsonSerializationContext(gson)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("test-bot", botConfig.account.name)
|
||||||
|
assertEquals("test-token", botConfig.account.token)
|
||||||
|
assertEquals(10000, botConfig.account.creatorId)
|
||||||
|
assertEquals(true, botConfig.enabled)
|
||||||
|
assertEquals(expectDefaultProxy, botConfig.proxy)
|
||||||
|
assertEquals(true, botConfig.disableBuiltInAbility)
|
||||||
|
assertEquals(true, botConfig.autoUpdateCommandList)
|
||||||
|
assertEquals("https://test.com", botConfig.baseApiUrl)
|
||||||
|
assertEquals(false, botConfig.extensions.isEmpty())
|
||||||
|
assertEquals(1, botConfig.extensions.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ProxyConfigSerializerTest {
|
||||||
|
|
||||||
|
// 测试 ProxyConfig 的 Json 序列化
|
||||||
|
@Test
|
||||||
|
fun `serialize test`() {
|
||||||
|
assertEquals(JsonNull.INSTANCE, ProxyConfigSerializer.serialize(null, null, null))
|
||||||
|
|
||||||
|
val expectDefaultConfig = ProxyConfig()
|
||||||
|
val actualDefaultJson = ProxyConfigSerializer.serialize(expectDefaultConfig, null, null)
|
||||||
|
assertTrue(actualDefaultJson is JsonObject)
|
||||||
|
assertEquals(expectDefaultConfig.type.name, actualDefaultJson["type"].asString)
|
||||||
|
assertEquals(expectDefaultConfig.host, actualDefaultJson["host"].asString)
|
||||||
|
assertEquals(expectDefaultConfig.port, actualDefaultJson["port"].asInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Bad type deserialize test`() {
|
||||||
|
val defaultConfig = ProxyConfig()
|
||||||
|
assertEquals(defaultConfig, ProxyConfigSerializer.deserialize(null, null, null))
|
||||||
|
assertEquals(defaultConfig, ProxyConfigSerializer.deserialize(JsonNull.INSTANCE, null, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deserialize test - object`() {
|
||||||
|
val defaultConfig = ProxyConfig()
|
||||||
|
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyConfigSerializer.deserialize(JsonArray(), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonWithoutType = JsonObject().apply {
|
||||||
|
addProperty("host", "example.com")
|
||||||
|
addProperty("port", 8080)
|
||||||
|
}
|
||||||
|
assertEquals(defaultConfig, ProxyConfigSerializer.deserialize(jsonWithoutType, null, null))
|
||||||
|
|
||||||
|
val looksGoodJson = JsonObject().apply {
|
||||||
|
addProperty("type", "HTTP")
|
||||||
|
addProperty("host", "example.com")
|
||||||
|
addProperty("port", 8080)
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
ProxyConfig(
|
||||||
|
type = ProxyType.HTTP,
|
||||||
|
host = "example.com",
|
||||||
|
port = 8080
|
||||||
|
), ProxyConfigSerializer.deserialize(looksGoodJson, null, null)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyConfigSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("type", "UNKNOWN")
|
||||||
|
addProperty("host", "example.com")
|
||||||
|
addProperty("port", 8080)
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyConfigSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("type", "HTTP")
|
||||||
|
addProperty("host", "example.com")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
ProxyConfigSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("type", "HTTP")
|
||||||
|
addProperty("port", 8080)
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ArtifactSerializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun badJsonType() {
|
||||||
|
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonObject(), null, null) }
|
||||||
|
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonArray(), null, null) }
|
||||||
|
assertFailsWith<JsonParseException> { ArtifactSerializer.deserialize(JsonPrimitive("A STRING"), null, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Basic format serialization`() {
|
||||||
|
val gav = "org.example.software:test:1.0.0-SNAPSHOT"
|
||||||
|
val expectArtifact = DefaultArtifact(gav)
|
||||||
|
val actualArtifact = DefaultArtifact(ArtifactSerializer.serialize(expectArtifact, null, null).asString)
|
||||||
|
assertEquals(expectArtifact, actualArtifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Full format serialization`() {
|
||||||
|
val gav = "org.example.software:test:war:javadoc:1.0.0-SNAPSHOT"
|
||||||
|
val expectArtifact = DefaultArtifact(gav)
|
||||||
|
val actualArtifact = DefaultArtifact(ArtifactSerializer.serialize(expectArtifact, null, null).asString)
|
||||||
|
assertEquals(expectArtifact, actualArtifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Bad format serialization`() {
|
||||||
|
assertFailsWith<JsonParseException> {
|
||||||
|
ArtifactSerializer.deserialize(JsonPrimitive("org.example~test"), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Other artifact implementation serialization`() {
|
||||||
|
val gav = "org.example.software:test:war:javadoc:1.0.0-SNAPSHOT"
|
||||||
|
val expectArtifact = DefaultArtifact(gav)
|
||||||
|
val otherArtifactImpl = mockk<Artifact> {
|
||||||
|
every { groupId } returns expectArtifact.groupId
|
||||||
|
every { artifactId } returns expectArtifact.artifactId
|
||||||
|
every { version } returns expectArtifact.version
|
||||||
|
every { classifier } returns expectArtifact.classifier
|
||||||
|
every { extension } returns expectArtifact.extension
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = ArtifactSerializer.serialize(otherArtifactImpl, null, null)
|
||||||
|
assertTrue(json is JsonPrimitive)
|
||||||
|
assertEquals(expectArtifact.toString(), json.asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deserialize() {
|
||||||
|
val gav = "org.example.software:test:war:javadoc:1.0.0-SNAPSHOT"
|
||||||
|
val expectArtifact = DefaultArtifact(gav)
|
||||||
|
val actualArtifact = ArtifactSerializer.deserialize(JsonPrimitive(gav), null, null)
|
||||||
|
assertEquals(expectArtifact, actualArtifact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UsernameAuthenticatorSerializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun serializeTest() {
|
||||||
|
val authenticator = UsernameAuthenticator("testUser", "testPassword")
|
||||||
|
val jsonElement = UsernameAuthenticatorSerializer.serialize(authenticator, null, null)
|
||||||
|
assertTrue(jsonElement.isJsonObject)
|
||||||
|
val jsonObject = jsonElement.asJsonObject
|
||||||
|
assertEquals("testUser", jsonObject["username"]?.asString)
|
||||||
|
assertEquals("testPassword", jsonObject["password"]?.asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deserializeTest() {
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonArray(), null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonPrimitive(""), null, null)
|
||||||
|
}
|
||||||
|
assertNull(UsernameAuthenticatorSerializer.deserialize(JsonNull.INSTANCE, null, null))
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("username", "testUser")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("username", "testUser")
|
||||||
|
add("password", JsonArray())
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("password", "testPassword")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
add("username", JsonArray())
|
||||||
|
addProperty("password", "testPassword")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("username", "")
|
||||||
|
addProperty("password", "")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("username", "testUser")
|
||||||
|
addProperty("password", "")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
org.junit.jupiter.api.assertThrows<JsonParseException> {
|
||||||
|
UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("username", "")
|
||||||
|
addProperty("password", "testPassword")
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val authenticator = UsernameAuthenticatorSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("username", "testUser")
|
||||||
|
addProperty("password", "testPassword")
|
||||||
|
}, null, null)
|
||||||
|
assertNotNull(authenticator)
|
||||||
|
|
||||||
|
assertTrue(authenticator.checkCredentials("testUser", "testPassword"))
|
||||||
|
assertFalse(authenticator.checkCredentials("falseUser", "testPassword"))
|
||||||
|
assertFalse(authenticator.checkCredentials("testUser", "falsePassword"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BotAccountSerializerTest {
|
||||||
|
|
||||||
|
private val expectToken = "123456789:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10"
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(BotAccount::class.java, BotAccountSerializer)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Invalid json type check test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(null, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonPrimitive("A STRING"), null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonArray(), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Field missing test`() {
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonObject(), null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("token", expectToken)
|
||||||
|
addProperty("creatorId", 1)
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("name", "testUser")
|
||||||
|
addProperty("creatorId", 1)
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(JsonParseException::class.java) {
|
||||||
|
BotAccountSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("name", "testUser")
|
||||||
|
addProperty("token", expectToken)
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val account = BotAccountSerializer.deserialize(JsonObject().apply {
|
||||||
|
addProperty("name", "testUser")
|
||||||
|
addProperty("token", expectToken)
|
||||||
|
addProperty("creatorId", 1)
|
||||||
|
}, null, null)
|
||||||
|
assertNotNull(account)
|
||||||
|
assertEquals("testUser", account.name)
|
||||||
|
assertEquals(expectToken, account.token)
|
||||||
|
assertEquals(1, account.creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `'token' check test`() {
|
||||||
|
val jsonObject = JsonObject().apply {
|
||||||
|
addProperty("name", "testUser")
|
||||||
|
addProperty("token", expectToken)
|
||||||
|
addProperty("creatorId", 123456789123456789)
|
||||||
|
}
|
||||||
|
|
||||||
|
val looksGoodAccount = BotAccountSerializer.deserialize(jsonObject, null, null)
|
||||||
|
|
||||||
|
assertNotNull(looksGoodAccount)
|
||||||
|
assertEquals("testUser", looksGoodAccount.name)
|
||||||
|
assertEquals(expectToken, looksGoodAccount.token)
|
||||||
|
assertEquals(123456789123456789, looksGoodAccount.creatorId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||||
|
addProperty("token", "")
|
||||||
|
}, null, null)
|
||||||
|
fail("Token 为空,但是没有抛出异常。")
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
assertEquals("`token` cannot be empty.", e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||||
|
addProperty("token", "abcdefghijklmnopqrstuvwxyz")
|
||||||
|
}, null, null)
|
||||||
|
fail("Token 格式错误(基本格式错误),但是没有抛出异常。")
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
assertEquals("`token` is invalid.", e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||||
|
addProperty("token", "abcdefgh:ijklmnopqrstuvwxyz-1234567890_abcde")
|
||||||
|
}, null, null)
|
||||||
|
fail("Token 格式错误(ID 不为数字),但是没有抛出异常。")
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
assertEquals("`token` is invalid.", e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||||
|
addProperty("token", "0123456789:ijklmnopqrstu-vwxyz_123456")
|
||||||
|
}, null, null)
|
||||||
|
fail("Token 格式错误(授权令牌长度错误),但是没有抛出异常。")
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
assertEquals("`token` is invalid.", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `'creatorId' check test`() {
|
||||||
|
val jsonObject = JsonObject().apply {
|
||||||
|
addProperty("name", "testUser")
|
||||||
|
addProperty("token", expectToken)
|
||||||
|
addProperty("creatorId", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val looksGoodAccount = gson.fromJson(jsonObject, BotAccount::class.java)
|
||||||
|
assertEquals(1, looksGoodAccount.creatorId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||||
|
addProperty("creatorId", "A STRING")
|
||||||
|
}, null, null)
|
||||||
|
fail("creatorId 不是一个数字,但是没有抛出异常。")
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
assertEquals("`creatorId` must be a number.", e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BotAccountSerializer.deserialize(jsonObject.deepCopy().apply {
|
||||||
|
addProperty("creatorId", -1)
|
||||||
|
}, null, null)
|
||||||
|
fail("creatorId 不能为负数,但是没有抛出异常。")
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
assertEquals("`creatorId` must be a positive number.", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `json deserialize test`() {
|
||||||
|
val jsonObject = JsonObject().apply {
|
||||||
|
addProperty("name", "testUser")
|
||||||
|
addProperty("token", expectToken)
|
||||||
|
addProperty("creatorId", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val looksGoodAccount = gson.fromJson(jsonObject, BotAccount::class.java)
|
||||||
|
|
||||||
|
assertNotNull(looksGoodAccount)
|
||||||
|
assertEquals("testUser", looksGoodAccount.name)
|
||||||
|
assertEquals(expectToken, looksGoodAccount.token)
|
||||||
|
assertEquals(1, looksGoodAccount.creatorId)
|
||||||
|
}
|
||||||
|
}
|
@ -4,3 +4,4 @@ rootProject.name = "scalabot"
|
|||||||
include(":scalabot-app")
|
include(":scalabot-app")
|
||||||
include(":scalabot-extension")
|
include(":scalabot-extension")
|
||||||
include("scalabot-ext-example")
|
include("scalabot-ext-example")
|
||||||
|
include("scalabot-meta")
|
||||||
|
Loading…
Reference in New Issue
Block a user