Compare commits

..
10 Commits
Author SHA1 Message Date
HibiKier 059b5c3a33 Merge pull request #169 from lhhxxxxx/run
[New Feature] en-US support
2022-05-17 20:42:14 +08:00
HibiKier 024f1e015b Merge pull request #174 from MingxuanGame/run
繁體中文支持
2022-05-17 20:40:07 +08:00
lhhxxxxxandKotoki 929bbe5095 Update README_en-US.md
Co-authored-by: Kotoki <Kotoki1337@gmail.com>
2022-05-17 20:38:59 +08:00
lhhxxxxxandKotoki b06795cfaf Update README.md
Co-authored-by: Kotoki <Kotoki1337@gmail.com>
2022-05-17 20:38:51 +08:00
MingxuanGame 1b51344332 繁體中文支持 2022-05-17 20:36:35 +08:00
lhhxxxxx c0abb730b6 Update README_en-US.md 2022-05-17 20:06:37 +08:00
lhhxxxxx 71e674efc0 Create README_en-US.md 2022-05-17 20:06:22 +08:00
lhhxxxxx af0d8415ae Update README.md 2022-05-17 19:57:25 +08:00
HibiKier c30f6b56c6 Update README.md 2022-05-17 19:36:21 +08:00
HibiKier 9eda3e4e48 run 2022-05-17 19:27:17 +08:00
430 changed files with 13 additions and 78539 deletions
-20
View File
@@ -1,20 +0,0 @@
.devcontainer/
.github/
.vscode/
.idea/
.pytest_cache/
.ruff_cache/
.venv/
docs_image/
k8s/
tests/
.dockerignore
.editorconfig
.gitignore
.pre-commit-config.yaml
.prettier*
.env.dev
docker-compose.yml
Dockerfile
LICENSE
*.md
-1
View File
@@ -1 +0,0 @@
ENVIRONMENT=dev
-93
View File
@@ -1,93 +0,0 @@
SUPERUSERS=[""]
COMMAND_START=[""]
SESSION_RUNNING_EXPRESSION="别急呀,小真寻要宕机了!QAQ"
NICKNAME=["真寻", "小真寻", "绪山真寻", "小寻子"]
SESSION_EXPIRE_TIMEOUT=00:00:30
ALCONNA_USE_COMMAND_START=True
# ws连接密钥,若bot能被公网访问则建议打开该注释并设置该配置项
# ONEBOT_ACCESS_TOKEN=""
# 全局图片统一使用bytes发送,当真寻与协议端不在同一服务器上时为True
IMAGE_TO_BYTES = True
# 回复消息时自称
SELF_NICKNAME="小真寻"
# 官bot appid:bot账号
QBOT_ID_DATA = '{
}'
# 数据库配置
# 示例: "postgres://user:password@127.0.0.1:5432/database"
# 示例: "mysql://user:password@127.0.0.1:3306/database"
# 示例: "sqlite:data/db/zhenxun.db" 在data目录下建立db文件夹
DB_URL = ""
# NONE: 不使用缓存, MEMORY: 使用内存缓存, REDIS: 使用Redis缓存
CACHE_MODE = NONE
# REDIS配置,使用REDIS替换Cache内存缓存
# REDIS地址
# REDIS_HOST = "127.0.0.1"
# REDIS端口
# REDIS_PORT = 6379
# REDIS密码
# REDIS_PASSWORD = ""
# REDIS过期时间
# REDIS_EXPIRE = 600
# 系统代理
# SYSTEM_PROXY = "http://127.0.0.1:7890"
PLATFORM_SUPERUSERS = '
{
"qq": [""],
"dodo": [""]
}
'
DRIVER=~fastapi+~httpx+~websockets
# LOG_LEVEL = DEBUG
# 服务器和端口
HOST = 127.0.0.1
PORT = 8080
# kook adapter toekn
# kaiheila_bots =[{"token": ""}]
# # discode adapter
# DISCORD_BOTS='
# [
# {
# "token": "",
# "intent": {
# "guild_messages": true,
# "direct_messages": true
# },
# "application_commands": {"*": ["*"]}
# }
# ]
# '
# DISCORD_PROXY=''
# # dodo adapter
# DODO_BOTS='
# [
# {
# "client_id": "",
# "token": ""
# }
# ]
# '
# application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令
# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
-98
View File
@@ -1,98 +0,0 @@
name: Bug 反馈
title: "Bug: "
description: 提交 Bug 反馈以帮助我们改进代码
labels: [ "bug" ]
body:
- type: checkboxes
id: checklist
attributes:
label: 提交前检查项
description: 在提交问题之前,请确认以下事项:
options:
- label: 我已搜索相关的 issue,但没有找到类似的问题
required: true
- label: 我已更新到最新版本(包括但不限于真寻本体,插件以及相关依赖),问题仍然存在
required: true
- label: 我已仔细阅读文档,确认我的配置正确
required: true
- type: dropdown
id: env-os
attributes:
label: 操作系统
description: 选择运行 zhenxun_bot 的系统
options:
- Windows
- MacOS
- Linux
- Other
validations:
required: true
- type: input
id: env-python-ver
attributes:
label: Python 版本
description: 填写运行 zhenxun_bot 的 Python 版本
placeholder: e.g. 3.11.0
validations:
required: true
- type: input
id: env-zhenxun-ver
attributes:
label: zhenxun_bot 版本
description: 填写 zhenxun_bot 版本
placeholder: e.g. 0.1.0
validations:
required: true
- type: input
id: env-adapter
attributes:
label: 适配器
description: 填写使用的适配器以及版本
placeholder: e.g. OneBot v11 2.2.2
validations:
required: true
- type: input
id: env-protocol
attributes:
label: 协议端
description: 填写连接 zhenxun_bot 的协议端及版本
placeholder: e.g. NapCat V4.0.3
validations:
required: true
- type: textarea
id: describe
attributes:
label: 描述问题
description: 清晰简洁地说明问题是什么
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 复现步骤
description: 提供能复现此问题的详细操作步骤
placeholder: |
1. 首先……
2. 然后……
3. 发生……
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望的结果
description: 清晰简洁地描述你期望发生的事情
- type: textarea
id: logs
attributes:
label: 截图或日志(请勿包含敏感信息如密码、令牌等)
description: 提供有助于诊断问题的任何日志和截图
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
-18
View File
@@ -1,18 +0,0 @@
name: 文档改进
title: "Docs: 描述"
description: 文档错误及改进意见反馈
labels: ["documentation"]
body:
- type: textarea
id: problem
attributes:
label: 描述问题或主题
validations:
required: true
- type: textarea
id: improve
attributes:
label: 需做出的修改
validations:
required: true
@@ -1,27 +0,0 @@
name: 功能建议
title: "Feature: 功能描述"
description: 提出关于项目新功能的想法
labels: [ "enhancement" ]
body:
- type: textarea
id: problem
attributes:
label: 希望能解决的问题
description: 在使用中遇到什么问题而需要新的功能?
validations:
required: true
- type: textarea
id: feature
attributes:
label: 描述所需要的功能
description: 请说明需要的功能或解决方法
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: 我有能力且愿意为这个功能贡献代码
options:
- label: 我有能力且愿意为这个功能贡献代码
-40
View File
@@ -1,40 +0,0 @@
name: Setup Python
description: Setup Python
inputs:
python-version:
description: Python version
required: false
default: "3.10"
env-dir:
description: Environment directory
required: false
default: "."
no-root:
description: Do not install package in the environment
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Install poetry
run: pipx install poetry
shell: bash
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: "poetry"
cache-dependency-path: |
./poetry.lock
${{ inputs.env-dir }}/poetry.lock
- run: |
cd ${{ inputs.env-dir }}
if [ "${{ inputs.no-root }}" = "true" ]; then
poetry install --all-extras --no-root
else
poetry install --all-extras
fi
shell: bash
-77
View File
@@ -1,77 +0,0 @@
template: $CHANGES
name-template: "v$RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
exclude-labels:
- reverted
- no-changelog
- skip-changelog
- invalid
autolabeler:
- label: "bug"
title:
- "/:bug:.+/"
- "/🐛.+/"
- label: "enhancement"
title:
- "/:sparkles:.+/"
- "/✨.+/"
- label: "ci"
files:
- .github/**/*
- label: "breaking-change"
title:
- "/.+!:.+/"
- label: "documentation"
files:
- "*.md"
- label: "dependencies"
files:
- "pyproject.toml"
- "requirements.txt"
- "poetry.lock"
title:
- "/:wrench:.+/"
- "/🔧.+/"
- label: "resources"
files:
- resources/**/*
categories:
- title: 💥 破坏性变更
labels:
- breaking-change
- title: 🚀 新功能
labels:
- enhancement
- title: 🐛 Bug 修复
labels:
- bug
- title: 📝 文档更新
labels:
- documentation
- title: 👻 自动化程序
labels:
- chore
- internal
- maintenance
- title: 🚦 测试
labels:
- test
- tests
- title: 📦 依赖更新
labels:
- dependencies
collapse-after: 15
- title: 💫 杂项
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- "major"
minor:
labels:
- "minor"
patch:
labels:
- "patch"
default: patch
-84
View File
@@ -1,84 +0,0 @@
name: 检查bot是否运行正常
on:
push:
branches: ["main"]
paths:
- zhenxun/**
- tests/**
- .github/workflows/bot_check.yml
- bot.py
pull_request:
branches: ["main"]
paths:
- zhenxun/**
- tests/**
- .github/workflows/bot_check.yml
- bot.py
jobs:
bot-check:
runs-on: ubuntu-latest
name: bot check
steps:
- uses: actions/checkout@v4
- name: Setup Python
id: setup_python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install Poetry
run: pip install poetry
# Poetry cache depends on OS, Python version and Poetry version.
- name: Cache Poetry cache
id: cache-poetry
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry
key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
- name: Cache playwright cache
id: cache-playwright
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}
- name: Cache Data cache
uses: actions/cache@v3
with:
path: data
key: data-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}
- name: Install dependencies
if: steps.cache-poetry.outputs.cache-hit != 'true'
run: |
rm -rf poetry.lock
poetry source remove aliyun
poetry install --no-root
- name: Install playwright
if: steps.cache-playwright.outputs.cache-hit != 'true'
run: |
poetry run sudo apt-get update
poetry run sudo apt-get install -y libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav flite x264 libx264-dev
poetry run pip install playwright
poetry run playwright install-deps
poetry run playwright install
- name: Run tests
run: poetry run pytest --cov=zhenxun --cov-report xml
- name: Check bot run
id: bot_check_run
run: |
mv scripts/bot_check.py bot_check.py
sed -i "s|^.*\?DB_URL.*|DB_URL=\"${{ env.DB_URL }}\"|g" .env.dev
sed -i "s/^.*\?LOG_LEVEL.*/LOG_LEVEL=${{ env.LOG_LEVEL }}/g" .env.dev
poetry run python3 bot_check.py
env:
DB_URL: "sqlite://:memory:"
LOG_LEVEL: DEBUG
-94
View File
@@ -1,94 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Code Security Analysis"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '45 21 * * 2'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: python
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
-11
View File
@@ -1,11 +0,0 @@
name: Sequential Lint and Type Check
on: [push, pull_request]
jobs:
ruff-call:
uses: ./.github/workflows/ruff.yml
pyright-call:
needs: ruff-call
uses: ./.github/workflows/pyright.yml
-58
View File
@@ -1,58 +0,0 @@
#
name: Create and publish a Docker image
# Configures this workflow to run on demand via workflow_dispatch.
on:
workflow_dispatch:
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
-55
View File
@@ -1,55 +0,0 @@
name: Pyright Lint
on:
workflow_call:
workflow_dispatch:
inputs:
python-version:
description: "Python version"
required: false
type: choice
options:
- "all"
- "3.10"
- "3.11"
- "3.12"
default: "all"
debug-mode:
description: "enable debug mode"
required: false
type: boolean
default: false
jobs:
pyright:
name: Pyright Lint
runs-on: ubuntu-latest
concurrency:
group: pyright-${{ github.ref }}-${{ matrix.env }}
cancel-in-progress: true
strategy:
matrix:
env: [pydantic-v1, pydantic-v2]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Setup Python environment
uses: ./.github/actions/setup-python
with:
env-dir: ./envs/${{ matrix.env }}
no-root: true
- run: |
(cd ./envs/${{ matrix.env }} && echo "$(poetry env info --path)/bin" >> $GITHUB_PATH)
if [ "${{ matrix.env }}" = "pydantic-v1" ]; then
sed -i 's/PYDANTIC_V2 = true/PYDANTIC_V2 = false/g' ./pyproject.toml
fi
shell: bash
- name: Run Pyright Check
uses: jakebailey/pyright-action@v2
with:
pylance-version: latest-release
-18
View File
@@ -1,18 +0,0 @@
name: Release Drafter
on:
push:
branches:
- main
- dev
pull_request:
types: [opened, reopened, synchronize]
jobs:
update_release_draft:
name: Update Release Draft
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
-20
View File
@@ -1,20 +0,0 @@
name: Ruff Lint
on:
workflow_call:
jobs:
ruff:
name: Ruff Lint
runs-on: ubuntu-latest
concurrency:
group: ruff-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Ruff
uses: astral-sh/ruff-action@v3
- name: Run Ruff Check
run: ruff check
-26
View File
@@ -1,26 +0,0 @@
name: Force Sync to Aliyun
on:
push:
branches: ["main"]
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config --global http.postBuffer 524288000
git config --global core.compression 0
- name: Add aliyun remote
run: |
git remote add aliyun https://${{secrets.ALIYUN_ACCOUNT}}:${{secrets.ALIYUN_PASSWORD}}@codeup.aliyun.com/67a361cf556e6cdab537117a/zhenxun-org/zhenxun_bot.git
git fetch aliyun main --force # 强制更新本地引用
- name: Force push
run: git push --progress --force aliyun HEAD:main
-73
View File
@@ -1,73 +0,0 @@
name: Update Version
on:
push:
paths:
- .github/workflows/update_version_pr.yml
- zhenxun/**
- resources/**
- bot.py
branches:
- main
- dev
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
- name: Read current version
id: read_version
run: |
version_line=$(grep '__version__' __version__)
version=$(echo $version_line | sed -E 's/__version__:\s*v([0-9]+\.[0-9]+\.[0-9]+)(-.+)?/\1/')
echo "Current version: $version"
echo "current_version=$version" >> $GITHUB_OUTPUT
- name: Check for version file changes
id: check_diff
run: |
if git diff --name-only HEAD~1 HEAD | grep -q '__version__'; then
echo "Version file has changes"
echo "version_changed=true" >> $GITHUB_OUTPUT
else
echo "Version file has no changes"
echo "version_changed=false" >> $GITHUB_OUTPUT
fi
- name: Get commit hash
id: get_commit_hash
run: echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Update version file
id: update_version
if: steps.check_diff.outputs.version_changed == 'false'
run: |
current_version="${{ steps.read_version.outputs.current_version }}"
commit_hash="${{ steps.get_commit_hash.outputs.commit_hash }}"
new_version="v${current_version}-${commit_hash}"
echo "new_version=$new_version" >> $GITHUB_OUTPUT
echo "Updating version to: $new_version"
echo "__version__: $new_version" > __version__
- name: Check updated version
if: steps.check_diff.outputs.version_changed == 'false'
run: cat __version__
- name: Create or update PR
if: steps.check_diff.outputs.version_changed == 'false'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GH_TOKEN }}
branch: create-pr/update_version
title: ":tada: chore(version): 自动更新版本到 ${{ steps.update_version.outputs.new_version }}"
body: "This PR updates the version file."
commit-message: ":tada: chore(version): Update version to ${{ steps.update_version.outputs.new_version }}"
add-paths: __version__
author: "AkashiCoin <i@loli.vet>"
committer: "${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>"
labels: automated-update
-150
View File
@@ -1,150 +0,0 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
!resources.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.env.dev
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
data/
log/
backup/
.idea/
resources/
.vscode/launch.json
./.env.dev
-4
View File
@@ -1,4 +0,0 @@
MD013: false
MD024: # 重复标题
siblings_only: true
MD033: false # 允许 html
-16
View File
@@ -1,16 +0,0 @@
default_install_hook_types: [pre-commit]
ci:
autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
autofix_prs: true
autoupdate_branch: main
autoupdate_schedule: monthly
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.2
hooks:
- id: ruff
args: [--fix]
stages: [pre-commit]
- id: ruff-format
stages: [pre-commit]
-8
View File
@@ -1,8 +0,0 @@
{
"recommendations": [
"charliermarsh.ruff",
"esbenp.prettier-vscode",
"ms-python.python",
"ms-python.vscode-pylance"
]
}
-65
View File
@@ -1,65 +0,0 @@
{
"C_Cpp.errorSquiggles": "enabled",
"terminal.integrated.env.linux": {
"PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}"
},
"cSpell.words": [
"aiofiles",
"Alconna",
"arclet",
"Arparma",
"displayname",
"flmt",
"getbbox",
"gitcode",
"GITEE",
"hibiapi",
"httpx",
"jsdelivr",
"kaiheila",
"lolicon",
"Mahiro",
"nonebot",
"onebot",
"pixiv",
"qbot",
"Setu",
"tobytes",
"ujson",
"unban",
"Uninfo",
"userinfo",
"webui",
"zhenxun"
],
"python.analysis.autoImportCompletions": true,
"python.testing.pytestArgs": ["tests"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.wordBasedSuggestions": "allDocuments",
"editor.formatOnType": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports": "explicit"
}
},
"ruff.format.preview": false,
"isort.check": true,
"ruff.importStrategy": "useBundled",
"ruff.organizeImports": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
-87
View File
@@ -1,87 +0,0 @@
# zhenxun_bot 贡献者公约
## 我们的承诺
身为社区成员、贡献者和负责人,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
## 我们的准则
有助于为我们的社区创造积极环境的行为例子包括但不限于:
* 表现出对他人的同情和善意
* 尊重不同的主张、观点和感受
* 提出和大方接受建设性意见
* 承担责任并向受我们错误影响的人道歉
* 注重社区共同诉求,而非个人得失
不当行为例子包括:
* 使用情色化的语言或图像,及性引诱或挑逗
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
* 公开或私下的骚扰行为
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
* 其他有理由认定为违反职业操守的不当行为
## 责任和权力
社区负责人有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
社区负责人有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。
## 适用范围
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
## 监督
辱骂、骚扰或其他不可接受的行为可通过 775757368@qq.com 向负责监督的社区负责人报告。
所有投诉都将得到及时和公平的审查和调查。
所有社区负责人都有义务尊重任何事件报告者的隐私和安全。
## 处理方针
社区负责人将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
### 1. 纠正
**社区影响**:使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
**处理意见**:由社区负责人发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
### 2. 警告
**社区影响**:单个或一系列违规行为。
**处理意见**:警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
### 3. 临时封禁
**社区影响**: 严重违反社区准则,包括持续的不当行为。
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
### 4. 永久封禁
**社区影响**:行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
**处理意见**:永久禁止在社区内进行任何形式的公开互动。
## 参见
本行为准则改编自 [Contributor Covenant][homepage] 2.1 版, 参见 [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]。
社区处理方针灵感来源于 [Mozilla's code of conduct enforcement ladder][Mozilla CoC]。
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。
其他语言翻译参见 [https://www.contributor-covenant.org/translations][translations]。
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
-95
View File
@@ -1,95 +0,0 @@
# zhenxun_bot 贡献指南
首先,感谢你愿意为 zhenxun_bot 贡献自己的一份力量!
本指南旨在引导你更规范地向 zhenxun_bot 提交贡献,请务必认真阅读。
## 提交 Issue
在提交 Issue 前,我们建议你先查看 [已有的 Issues](https://github.com/HibiKier/zhenxun_bot/issues),以防重复提交。
### 报告问题、故障与漏洞
如果你在使用过程中发现问题并确信是由 zhenxun_bot 引起的,欢迎提交 Issue。
请使用我们提供的 **Bug 反馈** 模板,并尽可能详细地描述:
- 问题描述
- 重现步骤
- 你的环境信息(如操作系统、依赖版本等)
### 建议功能
如果你有新的功能需求或改进建议,欢迎提出。
请使用 **功能建议** 模板,并详细描述你所需要的特性,可能的话可以提出你认为可行的解决方案。
### 文档相关
如果你觉得文档有误或缺乏更新,欢迎提出。
请使用 **文档改进** 模板,并详细描述问题或主题,希望我们做出的修改
## Pull Request
### 分支管理
请从 `main` 分支创建新功能分支,例如:
- 新功能:`feature/功能描述`
- 问题修复:`bugfix/问题描述`
### 代码风格
zhenxun_bot 使用 `pre-commit` 进行代码格式化和检查,请在提交前确保代码通过检查。
```bash
# 在安装项目依赖后安装 pre-commit 钩子
pre-commit install
```
> 未通过 `pre-commit` 检查的代码将无法合并。
### Commit 规范
请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个目的。
我们建议遵循 [gitmoji](https://gitmoji.dev/) 的 commit message 格式,在创建 commit 时请牢记这一点。
### 工作流程概述
`main` 分支为 zhenxun_bot 的主分支,在任何情况下都请不要直接修改 `main` 分支,而是创建一个目标分支为 `main` 的 Pull Request 来提交修改。Pull Request 标题请尽量清晰,以便维护者进行审核。
如果你不是 zhenxun_bot 团队的成员,可在 fork 本仓库后,向本仓库的 `main` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后合并你的贡献。
### 撰写文档
如果你对文档有改进建议,欢迎提交 Pull Request 或者 Issue。
[//]: # (我们使用 Markdown 编写文档,建议遵循以下规范:)
[//]: # ()
[//]: # (1. 中文与英文、数字、半角符号之间需要有空格。例:`zhenxun_bot 是一个高效的聊天机器人。`)
[//]: # (2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello,世界!”。`)
[//]: # (3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。)
[//]: # (4. **不要使用斜体**,你不需要一种与粗体不同的强调。)
[//]: # (5. 文档中应以“我们”指代开发者,以“用户”指代机器人的使用者。)
[//]: # ()
[//]: # (如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 `markdownlint` 扩展。)
### 参与开发
zhenxun_bot 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。
> 暂未搭建测试框架,因此暂不要求添加测试代码。
## 项目沟通
如有关于贡献流程的疑问或需要进一步指导,请通过 [QQ群](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) 联系我们。
再次感谢你的贡献!
-67
View File
@@ -1,67 +0,0 @@
FROM python:3.11-bookworm AS requirements-stage
WORKDIR /tmp
ENV POETRY_HOME="/opt/poetry" PATH="${PATH}:/opt/poetry/bin"
RUN curl -sSL https://install.python-poetry.org | python - -y && \
poetry self add poetry-plugin-export
COPY ./pyproject.toml ./poetry.lock* /tmp/
RUN poetry export \
-f requirements.txt \
--output requirements.txt \
--without-hashes \
--without-urls
FROM python:3.11-bookworm AS build-stage
WORKDIR /wheel
COPY --from=requirements-stage /tmp/requirements.txt /wheel/requirements.txt
# RUN python3 -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple
RUN pip wheel --wheel-dir=/wheel --no-cache-dir --requirement /wheel/requirements.txt
FROM python:3.11-bookworm AS metadata-stage
WORKDIR /tmp
RUN --mount=type=bind,source=./.git/,target=/tmp/.git/ \
git describe --tags --exact-match > /tmp/VERSION 2>/dev/null \
|| git rev-parse --short HEAD > /tmp/VERSION \
&& echo "Building version: $(cat /tmp/VERSION)"
FROM python:3.11-slim-bookworm
WORKDIR /app/zhenxun
ENV TZ=Asia/Shanghai PYTHONUNBUFFERED=1
#COPY ./scripts/docker/start.sh /start.sh
#RUN chmod +x /start.sh
EXPOSE 8080
RUN apt update && \
apt install -y --no-install-recommends curl fontconfig fonts-noto-color-emoji \
&& apt clean \
&& fc-cache -fv \
&& apt-get purge -y --auto-remove curl \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖项和应用代码
COPY --from=build-stage /wheel /wheel
COPY . .
RUN pip install --no-cache-dir --no-index --find-links=/wheel -r /wheel/requirements.txt && rm -rf /wheel
RUN playwright install --with-deps chromium \
&& rm -rf /var/lib/apt/lists/* /tmp/*
COPY --from=metadata-stage /tmp/VERSION /app/VERSION
VOLUME ["/app/zhenxun/data", "/app/zhenxun/resources", "/app/zhenxun/log"]
CMD ["python", "bot.py"]
-661
View File
@@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+4 -427
View File
@@ -1,429 +1,6 @@
<!-- markdownlint-disable MD033 MD041 -->
<div align=center>
## 要不跑路吧
<img width="250" height="312" src=./docs_image/tt.jpg alt="zhenxun_bot"/>
**中文** | [English](README_en-US.md)
</div>
<div align=center>
<a href="./LICENSE">
<img src="https://img.shields.io/badge/license-AGPL3.0-FE7D37" alt="license">
</a>
<a href="https://www.python.org">
<img src="https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12-blue" alt="python">
</a>
<a href="https://nonebot.dev/">
<img src="https://img.shields.io/badge/nonebot-v2.1.3-EA5252" alt="nonebot">
</a>
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
</a>
<a href="https://bot.q.qq.com/wiki/">
<img src="https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ">
</a>
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
</a>
<a href="https://github.com/Microsoft/pyright">
<img src="https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641" alt="pyright">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
</a>
</div>
<div align=center>
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-是真寻酱哒-red?style=logo=tencent-qq)](https://qm.qq.com/q/mRNtLSl6uc)
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-真寻的技术群-c73e7e?style=logo=tencent-qq)](https://qm.qq.com/q/YYYt5rkMYc)
</div>
<div align=center>
[文档](https://zhenxun-org.github.io/zhenxun_bot/)
</div>
<div align=center>
## 绪山真寻 Bot
</div>
<div align=center>
“真寻是<strong>[椛椛](https://github.com/FloatTech/ZeroBot-Plugin)</strong>的好朋友!”
🎉 喜欢真寻,于是真寻就来了!🎉
本项目符合 [OneBot](https://github.com/howmanybots/onebot) 标准,可基于以下项目与机器人框架/平台进行交互
| 项目地址 | 平台 | 核心作者 | 备注 |
| :-----------------------------------------------------------: | :--: | :----------------------: | :--: |
| [LLOneBot](https://github.com/LLOneBot/LLOneBot) | NTQQ | linyuchen | 可用 |
| [Napcat](https://github.com/NapNeko/NapCatQQ) | NTQQ | NapNeko | 可用 |
| [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) | NTQQ | LagrangeDev/Linwenxuan04 | 可用 |
</div>
<div align=center>
<img width="100%" src="https://starify.komoridevs.icu/api/starify?owner=HibiKier&repo=zhenxun_bot" alt="starify" />
<img src="https://api.star-history.com/svg?repos=HibiKier/zhenxun_bot&type=Timeline" alt="Star Trend" width="800" />
</div>
## 🤝 帮助页面
<details>
<summary>点击展开查看图片</summary>
<img width="300" height="auto" src="./docs_image/zhenxun_help.png" alt="zhenxun_help"/>
<img width="300" height="auto" src="./docs_image/html_help.png" alt="html_help"/>
<img width="300" height="auto" src="./docs_image/help.png" alt="help"/>
</details>
## 📦 这是一份扩展
### 1. 体验一下?
这是一个免费的,版本为 dev 的 zhenxun,你可以通过 [napcat](https://github.com/NapNeko/NapCatQQ) 或 [拉格朗日](https://github.com/LagrangeDev/Lagrange.Core) 以及 [matcha](https://github.com/A-kirami/matcha) 等直接连接用于体验与测试
(球球了测试君!)
```text
Url: ws://test.zhenxun.org:8080/onebot/v11/ws
AccessToken: PUBLIC_ZHENXUN_TEST
注:你无法获得超级用户权限
```
### 2. 额外扩展
<div align=center>
“不要害怕,你的背后还有千千万万的 <strong>伙伴</strong> 啊!”
| 项目名称 | 主要用途 | 仓库作者 | 备注 |
| :--------------------------------------------------------------------: | :------: | :-------------------------------------------------: | :---------------------------------------------------: |
| [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 |
| [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 |
| [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 |
| [WebUi](https://github.com/zhenxun-org/zhenxun_bot) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
| [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 |
</div>
## 🥰 ~~来点优点?~~ 可爱难道还不够吗
- 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件
- 拥有完善可用的 webui
- 通过 Config 配置项将所有插件配置统一保存至 config.yaml,利于统一用户修改
- 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息`
- 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等
- **更多详细请通过 [传送门](https://zhenxun-org.github.io/zhenxun_bot/) 查看文档!**
## 🐣 小白整合
如果你系统是 **Windows** 且不想下载 Python
可以使用整合包(Python3.10+zhenxun+webui)
文档地址:[整合包文档](https://zhenxun-org.github.io/zhenxun_bot/beginner)
<details>
<summary>下载地址</summary>
- **百度云:**
https://pan.baidu.com/s/1MKGOoIgQW1qom_KT3rNhlg?pwd=t7iz
- **夸克网盘:**
https://pan.quark.cn/s/b4dc6cb8fb08
访问码:Yi46
- **Google Drive:**
https://drive.google.com/drive/folders/1dcTWhPZhSt9WqLehyjF6Gj0CXQCS4OWh?usp=drive_link
</details>
## 🛠️ 简单部署
```bash
# 获取代码
git clone https://github.com/HibiKier/zhenxun_bot.git
# 进入目录
cd zhenxun_bot
# 安装依赖
pip install poetry # 安装 poetry
poetry install # 安装依赖
# 开始运行
poetry run python bot.py
```
## 📝 简单配置
> [!TIP]
> config.yaml 需要启动一次 Bot 后生成
1.在 .env.dev 文件中填写你的机器人配置项
2.在 data/config.yaml 文件中修改你需要修改的插件配置项
<details>
<summary>数据库地址(DB_URL)配置说明</summary>
DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目所使用的数据库。以下是 DB_URL 的组成部分以及示例:
格式为: `<数据库类型>://<用户名>:<密码>@<主机>:<端口>/<数据库名>?<参数>`
- 数据库类型:表示数据库类型,例如 postgres、mysql、sqlite 等。
- 用户名:数据库的用户名,例如 root。
- 密码:数据库的密码,例如 123456。
- 主机:数据库的主机地址,例如 127.0.0.1(本地)或远程服务器 IP。
- 端口:数据库的端口号,例如:PostgreSQL:5432, MySQL:3306
- 数据库名:指定要使用的数据库名称,例如 zhenxun。
- 参数(可选):用于传递额外的配置,例如字符集设置。
</details>
## 📋 功能列表
> [!NOTE]
> 真寻原 `plugins` 插件文件夹已迁移至 [插件仓库](https://github.com/zhenxun-org/zhenxun_bot_plugins) ,现在本体仅保留核心功能
<details>
<summary>内置功能</summary>
### 🔧 基础功能
- 昵称系统(群与群与私聊分开)
- 签到/我的签到/好感度排行/好感度总排行(影响色图概率和开箱次数,支持配置)
- 商店/我的金币/购买道具/使用道具/金币排行(完整的商店添加/购买/使用流程)
- 查看当前群欢迎消息
- 个人信息查看(群组内权限,聊天频率等)
- 消息撤回
- 功能统计可视化
- 关于
- 三种样式的帮助菜单
### 🛠️ 管理员功能
- 管理员帮助
- 更新群组成员信息
- 95%的群功能开关
- 查看群内被动技能状态
- 自定义群欢迎消息(是真寻的不是管家的!)
- ban/unban(支持设置 ban 时长)= 群组及用户的黑名单
- 休息吧/醒来(群组内真寻状态)
### 🧑‍💼 超级用户功能
- 超级用户帮助
- 添加/删除权限(是真寻的管理员权限,不是群管理员)
- 群组管理,退群指令等
- 广播
- 自检(检查系统状态)
- 所有群组/所有好友
- 退出指定群
- 更新好友信息/更新群信息
- 修改群权限
- 检查更新
- 重启
- 添加/删除/查看群白名单
- 功能开关(更多设置)
- 功能状态
- 执行 SQL
- 重载配置
- 清理临时数据
- 增删群认证
- 同意/拒绝好友/群聊请求
- 添加/移除/更新插件/插件商店(plugins 库以及扩展库)
- WebUI API(对真寻前端的支持)
#### 🛡️ 超级用户的被动技能
- 邀请入群提醒(别人邀请真寻入群,可配置自动同意)
- 添加好友提醒(别人添加真寻好友,可配置自动同意)
### 🤖 被动技能
- 群早晚安
### 👻 看不见的技能
- 功能调用统计
- 聊天记录统计
- 检测恶意触发命令(将被最高权限 ban 掉 30 分钟,只有最高权限(9 级)可以进行 unban)
- 自动同意好友/群组请求,加群请求将会提醒管理员,退群提示,加群欢迎等等
- 群聊时间检测(当群聊最后一人发言时间大于当前 48 小时后将关闭该群所有通知(即被动技能))
- 群管理员监控,自动为新晋管理员增加权限,为失去群管理员的用户删除权限
- 群权限系统
- 定时更新权限
- 自动配置重载
- 强制入群保护
- 自定备份(可配置)
- 笨蛋检测(当使用功能名称当指令时真寻会跳出来狠狠嘲笑并帮助)
</details>
## 💖 赞助
<details>
<summary>爱发电</summary>
<a href="https://afdian.com/a/HibiKier">
<img width="365px" height="450px" src=./docs_image/afd.jpg>
</a>
</details>
### 赞助名单
(可以告诉我你的 **github** 地址,我偷偷换掉 0v|)
[Zer](https://afdian.com/u/6bccdb2a60b411ec9ad452540025c377) [爱发电用户\_HTjk](https://afdian.com/u/6c7d0208064511ec8d7b52540025c377) [shenghuo2](https://afdian.com/u/bca13286102111eda2a052540025c377) [术樱](https://afdian.com/u/414da63a09a311ec8eb752540025c377) [飞火](https://afdian.com/u/404135f48ed711ec962152540025c377) [shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) [A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377) [疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377) [投冥](https://afdian.net/a/144514mm) [茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377) [AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377) [爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377) [qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377) [溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377) [伝木](https://afdian.net/u/246b80683f9511edba7552540025c377) [阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377) [醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377) [Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377) [本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377) [椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377) [kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377) [咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377) [Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377) [墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377) [爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [noahzark](https://afdian.net/a/noahzark) [腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377) [ze roller](https://afdian.net/u/0e599e96257211ed805152540025c377) [爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377) [爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377) [烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377) [ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377) [爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377) [爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377) [爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377) [MangataAkihi](https://github.com/Sakuracio) [炀](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377) [爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377) [大魔王](https://github.com/xipesoy) [CopilotLaLaLa](https://github.com/CopilotLaLaLa) [嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377) [回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377) [十年くん](https://github.com/shinianj) [哇](https://afdian.net/u/9b266244f23911eca19052540025c377) [yajiwa](https://github.com/yajiwa) [爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377)...
### 特别赞助
<div align=center>
<img width="60%" src="https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png" />
[亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
**本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助**
</div>
## 📜 贡献指南
欢迎查看我们的 [贡献指南](CONTRIBUTING.md) 和 [行为守则](CODE_OF_CONDUCT.md) 以了解如何参与贡献。
## ❔ 需要帮助?
> [!TIP]
> 发起 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
>
> - 善用[搜索引擎](https://www.google.com/)
> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue
欢迎前往 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
## 🛠️ 进度追踪
Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
## 🌟 特别感谢
首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss)
LOGO 设计:[FrostN0v0](https://github.com/FrostN0v0)
## 🙏 感谢
[botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议
[Mrs4s / go-cqhttp](https://github.com/Mrs4s/go-cqhttp) :cqhttp 的 golang 实现,轻量、原生跨平台.
[nonebot / nonebot2](https://github.com/nonebot/nonebot2) :跨平台 Python 异步机器人框架
[Angel-Hair / XUN_Bot](https://github.com/Angel-Hair/XUN_Bot) :一个基于 NoneBot 和酷 Q 的功能性 QQ 机器人
[pcrbot / cappuccilo_plugins](https://github.com/pcrbot/cappuccilo_plugins) :hoshino 插件合集
[MeetWq /nonebot-plugin-withdraw](https://github.com/MeetWq/nonebot-plugin-withdraw) :A simple withdraw plugin for Nonebot2
[maxesisn / nonebot_plugin_songpicker2](https://github.com/maxesisn/nonebot_plugin_songpicker2) :适用于 nonebot2 的点歌插件
[nonepkg / nonebot-plugin-manager](https://github.com/nonepkg/nonebot-plugin-manager) :Nonebot Plugin Manager base on import hook
[H-K-Y / Genshin_Impact_bot](https://github.com/H-K-Y/Genshin_Impact_bot) :原神 bot,这是一个基于 nonebot 和 HoshinoBot 的原神娱乐及信息查询插件
[NothAmor / nonebot2_luxun_says](https://github.com/NothAmor/nonebot2_luxun_says) :基于 nonebot2 机器人框架的鲁迅说插件
[Kyomotoi / AnimeThesaurus](https://github.com/Kyomotoi/AnimeThesaurus) :一个~~特二刺螈~~(文爱)的适用于任何 bot 的词库
[Ailitonia / omega-miya](https://github.com/Ailitonia/omega-miya) :基于 nonebot2 的 qq 机器人
[KimigaiiWuyi / GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) :一个基于 HoshinoBot/NoneBot2 的原神 UID 查询插件
## 📊 统计与活跃贡献者
<a href="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats?repo_id=368008334" target="_blank" style="display: block" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=368008334&image_size=auto&color_scheme=dark" width="800" height="auto">
<img alt="Performance Stats of HibiKier/zhenxun_bot - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=368008334&image_size=auto&color_scheme=light" width="800" height="auto">
</picture>
</a>
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=368008334&limit=30" target="_blank" style="display: block" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=368008334&limit=30&image_size=auto&color_scheme=dark" width="800" height="auto">
<img alt="Active Contributors of HibiKier/zhenxun_bot - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=368008334&limit=30&image_size=auto&color_scheme=light" width="800" height="auto">
</picture>
</a>
## 👨‍💻 开发者
感谢以下开发者对 绪山真寻 Bot 作出的贡献:
<a href="https://github.com/HibiKier/zhenxun_bot/graphs/contributors" style="display: block" align="center">
<img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/>
</a>
## 📸 WebUI 界面展示(仅展示默认主题下的 pc 端)
<div style="display: flex; flex-wrap: wrap; justify-content: space-between;">
#### 登录界面
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-login.jpg)
#### API 设置
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-api.jpg)
#### 仪表盘
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-dashboard.jpg)
#### 仪表盘(展开)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-dashboard1.jpg)
#### 控制台
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-command.jpg)
#### 插件列表
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-plugin.jpg)
#### 插件列表(配置项)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-plugin1.jpg)
#### 插件商店
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-store.jpg)
#### 好友/群组管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-manage.jpg)
#### 请求管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-manage1.jpg)
#### 数据库管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-database.jpg)
### 文件管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system.jpg)
### 文件管理(文本查看)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system1.jpg)
### 文件管理(图片查看)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system2.jpg)
### 关于
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-about.jpg)
</div>
代码和人有一个能run就行了
已经坐船到柬埔寨了,马上转船到金三角了(
+6
View File
@@ -0,0 +1,6 @@
## I'D BETTER RUN
[中文](README.md) | **English**
Either code or me runable is fine
Already took the boat to Cambodia, and will transfer to the Golden Triangle soon(
+3
View File
@@ -0,0 +1,3 @@
## 要不跑路吧
代碼和人有一個能run就行了
已經坐船到柬埔寨了,馬上轉船到金三角了(
-1
View File
@@ -1 +0,0 @@
__version__: v0.2.4-da6d5b4
-28
View File
@@ -1,28 +0,0 @@
import nonebot
# from nonebot.adapters.discord import Adapter as DiscordAdapter
# from nonebot.adapters.dodo import Adapter as DoDoAdapter
# from nonebot.adapters.kaiheila import Adapter as KaiheilaAdapter
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
nonebot.init()
driver = nonebot.get_driver()
driver.register_adapter(OneBotV11Adapter)
# driver.register_adapter(KaiheilaAdapter)
# driver.register_adapter(DoDoAdapter)
# driver.register_adapter(DiscordAdapter)
from zhenxun.services.db_context import disconnect
# driver.on_startup(init)
driver.on_shutdown(disconnect)
# nonebot.load_builtin_plugins("echo")
nonebot.load_plugins("zhenxun/builtin_plugins")
nonebot.load_plugins("zhenxun/plugins")
if __name__ == "__main__":
nonebot.run()
-67
View File
@@ -1,67 +0,0 @@
services:
db:
image: postgres:15
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: zhenxun
volumes:
- pgdata:/var/lib/postgresql/data
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9187"
postgres-exporter:
image: prometheuscommunity/postgres-exporter
environment:
DATA_SOURCE_NAME: "postgresql://postgres:password@db:5432/zhenxun?sslmode=disable"
ports:
- "9187:9187"
depends_on:
- db
redis:
image: redis:7
ports:
- "6379:6379"
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9121"
redis-exporter:
image: oliver006/redis_exporter
environment:
REDIS_ADDR: redis://redis:6379
ports:
- "9121:9121"
depends_on:
- redis
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
grafana:
image: grafana/grafana
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- prometheus
volumes:
pgdata:
prometheus_data:
grafana_data:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 708 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 KiB

-5578
View File
File diff suppressed because it is too large Load Diff
-145
View File
@@ -1,145 +0,0 @@
[tool.poetry]
name = "zhenxun_bot"
version = "0.2.4"
description = "基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot"
authors = ["HibiKier <775757368@qq.com>"]
license = "AGPL"
package-mode = false
[[tool.poetry.source]]
name = "aliyun"
url = "https://mirrors.aliyun.com/pypi/simple/"
priority = "primary"
[tool.poetry.dependencies]
python = "^3.10"
playwright = "^1.41.1"
nonebot-adapter-onebot = ">=2.3.1"
nonebot-plugin-apscheduler = "^0.5"
tortoise-orm = "^0.20.0"
cattrs = "^23.2.3"
ruamel-yaml = "^0.18.5"
strenum = "^0.4.15"
nonebot-plugin-session = "^0.3.2"
ujson = ">=5.9.0"
nb-cli = ">=1.3.0"
nonebot2 = { extras = ["fastapi"], version = ">=2.3.3" }
pillow = "^10.0.0"
retrying = "^1.3.4"
aiofiles = "^23.2.1"
nonebot-plugin-htmlrender = ">=0.6.0,<1.0.0"
pypinyin = ">=0.51.0"
beautifulsoup4 = "^4.12.3"
lxml = "^5.1.0"
psutil = "^5.9.8"
feedparser = "^6.0.11"
imagehash = "^4.3.1"
cn2an = "^0.5.22"
dateparser = "^1.2.0"
bilireq = ">=0.2.10"
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
python-multipart = "^0.0.9"
aiocache = {extras = ["redis"], version = "^0.12.3"}
py-cpuinfo = "^9.0.0"
nonebot-plugin-alconna = ">=0.56.0"
tenacity = "^9.0.0"
nonebot-plugin-uninfo = ">=0.7.3"
nonebot-plugin-waiter = "^0.8.1"
multidict = ">=6.0.0,!=6.3.2"
pydantic = ">=1.0.0, <2.0.0"
redis = { version = ">=5", optional = true }
asyncpg = { version = ">=0.20.0", optional = true }
alibabacloud-devops20210625 = "^5.0.2"
[tool.poetry.group.dev.dependencies]
nonebug = "^0.4"
pytest-cov = "^5.0.0"
pytest-mock = "^3.6.1"
pytest-asyncio = "^0.25"
pytest-xdist = "^3.3.1"
respx = "^0.21.1"
ruff = "^0.8.0"
pre-commit = "^4.0.0"
[tool.nonebot]
plugins = [
"nonebot_plugin_apscheduler",
"nonebot_plugin_session",
"nonebot_plugin_htmlrender",
"nonebot_plugin_alconna",
]
plugin_dirs = ["zhenxun/services", "zhenxun/builtin_plugins", "zhenxun/plugins"]
adapters = [
{ name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
# { name = "DoDo", module_name = "nonebot.adapters.dodo" },
# { name = "开黑啦", module_name = "nonebot.adapters.kaiheila" },
]
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint]
select = [
"F", # Pyflakes
"W", # pycodestyle warnings
"E", # pycodestyle errors
"I", # isort
"UP", # pyupgrade
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
"T10", # flake8-debugger
"T20", # flake8-print
"PYI", # flake8-pyi
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"TID", # flake8-tidy-imports
"RUF", # Ruff-specific rules
]
ignore = [
"E402", # module-import-not-at-top-of-file
"UP037", # quoted-annotation
"RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring
"RUF003", # ambiguous-unicode-character-comment
"TID252", # relative-imports
]
[tool.ruff.lint.isort]
force-sort-within-sections = true
known-first-party = ["zhenxun", "tests/*"]
extra-standard-library = ["typing_extensions"]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[tool.pyright]
pythonVersion = "3.10"
pythonPlatform = "All"
defineConstant = { PYDANTIC_V2 = true }
executionEnvironments = [
{ root = "./tests", extraPaths = [
"./",
] },
{ root = "./" },
]
typeCheckingMode = "standard"
reportShadowedImports = false
disableBytesTypePromotions = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
-5688
View File
File diff suppressed because it is too large Load Diff
-146
View File
@@ -1,146 +0,0 @@
[tool.poetry]
name = "zhenxun_bot"
version = "0.2.4"
description = "基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot"
authors = ["HibiKier <775757368@qq.com>"]
license = "AGPL"
package-mode = false
[[tool.poetry.source]]
name = "aliyun"
url = "https://mirrors.aliyun.com/pypi/simple/"
priority = "primary"
[tool.poetry.dependencies]
python = "^3.10"
playwright = "^1.41.1"
nonebot-adapter-onebot = ">=2.3.1"
nonebot-plugin-apscheduler = "^0.5"
tortoise-orm = "^0.20.0"
cattrs = "^23.2.3"
ruamel-yaml = "^0.18.5"
strenum = "^0.4.15"
nonebot-plugin-session = "^0.3.2"
ujson = ">=5.9.0"
nb-cli = ">=1.3.0"
nonebot2 = { extras = ["fastapi"], version = ">=2.3.3" }
pillow = "^10.0.0"
retrying = "^1.3.4"
aiofiles = "^23.2.1"
nonebot-plugin-htmlrender = ">=0.6.0,<1.0.0"
pypinyin = ">=0.51.0"
beautifulsoup4 = "^4.12.3"
lxml = "^5.1.0"
psutil = "^5.9.8"
feedparser = "^6.0.11"
imagehash = "^4.3.1"
cn2an = "^0.5.22"
dateparser = "^1.2.0"
bilireq = ">=0.2.10"
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
python-multipart = "^0.0.9"
aiocache = {extras = ["redis"], version = "^0.12.3"}
py-cpuinfo = "^9.0.0"
nonebot-plugin-alconna = ">=0.56.0"
tenacity = "^9.0.0"
nonebot-plugin-uninfo = ">=0.7.3"
nonebot-plugin-waiter = "^0.8.1"
multidict = ">=6.0.0,!=6.3.2"
pydantic = ">=2.0.0, <3.0.0"
redis = { version = ">=5", optional = true }
asyncpg = { version = ">=0.20.0", optional = true }
alibabacloud-devops20210625 = "^5.0.2"
[tool.poetry.group.dev.dependencies]
nonebug = "^0.4"
pytest-cov = "^5.0.0"
pytest-mock = "^3.6.1"
pytest-asyncio = "^0.25"
pytest-xdist = "^3.3.1"
respx = "^0.21.1"
ruff = "^0.8.0"
pre-commit = "^4.0.0"
[tool.nonebot]
plugins = [
"nonebot_plugin_apscheduler",
"nonebot_plugin_session",
"nonebot_plugin_htmlrender",
"nonebot_plugin_alconna",
]
plugin_dirs = ["zhenxun/services", "zhenxun/builtin_plugins", "zhenxun/plugins"]
adapters = [
{ name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
# { name = "DoDo", module_name = "nonebot.adapters.dodo" },
# { name = "开黑啦", module_name = "nonebot.adapters.kaiheila" },
]
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint]
select = [
"F", # Pyflakes
"W", # pycodestyle warnings
"E", # pycodestyle errors
"I", # isort
"UP", # pyupgrade
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
"T10", # flake8-debugger
"T20", # flake8-print
"PYI", # flake8-pyi
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"TID", # flake8-tidy-imports
"RUF", # Ruff-specific rules
]
ignore = [
"E402", # module-import-not-at-top-of-file
"UP037", # quoted-annotation
"RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring
"RUF003", # ambiguous-unicode-character-comment
"TID252", # relative-imports
]
[tool.ruff.lint.isort]
force-sort-within-sections = true
known-first-party = ["zhenxun", "tests/*"]
extra-standard-library = ["typing_extensions"]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[tool.pyright]
pythonVersion = "3.10"
pythonPlatform = "All"
defineConstant = { PYDANTIC_V2 = true }
executionEnvironments = [
{ root = "./tests", extraPaths = [
"./",
] },
{ root = "./" },
]
typeCheckingMode = "standard"
reportShadowedImports = false
disableBytesTypePromotions = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Generated
-5693
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -1,12 +0,0 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'postgresql'
static_configs:
- targets: [ 'postgres-exporter:9187' ]
- job_name: 'redis'
static_configs:
- targets: [ 'redis-exporter:9121' ]
-148
View File
@@ -1,148 +0,0 @@
[tool.poetry]
name = "zhenxun_bot"
version = "0.2.4"
description = "基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot"
authors = ["HibiKier <775757368@qq.com>"]
license = "AGPL"
package-mode = false
[[tool.poetry.source]]
name = "aliyun"
url = "https://mirrors.aliyun.com/pypi/simple/"
priority = "primary"
[tool.poetry.dependencies]
python = "^3.10"
playwright = "^1.41.1"
nonebot-adapter-onebot = ">=2.3.1"
nonebot-plugin-apscheduler = "^0.5"
tortoise-orm = "^0.20.0"
cattrs = "^23.2.3"
ruamel-yaml = "^0.18.5"
strenum = "^0.4.15"
nonebot-plugin-session = "^0.3.2"
ujson = ">=5.9.0"
nb-cli = ">=1.3.0"
nonebot2 = { extras = ["fastapi"], version = ">=2.3.3" }
pillow = "^10.0.0"
retrying = "^1.3.4"
aiofiles = "^23.2.1"
nonebot-plugin-htmlrender = ">=0.6.0,<1.0.0"
pypinyin = ">=0.51.0"
beautifulsoup4 = "^4.12.3"
lxml = "^5.1.0"
psutil = "^5.9.8"
feedparser = "^6.0.11"
imagehash = "^4.3.1"
cn2an = "^0.5.22"
dateparser = "^1.2.0"
bilireq = ">=0.2.10"
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
python-multipart = "^0.0.9"
aiocache = {extras = ["redis"], version = "^0.12.3"}
py-cpuinfo = "^9.0.0"
nonebot-plugin-alconna = ">=0.56.0"
tenacity = "^9.0.0"
nonebot-plugin-uninfo = ">=0.7.3"
nonebot-plugin-waiter = "^0.8.1"
multidict = ">=6.0.0,!=6.3.2"
redis = { version = ">=5", optional = true }
asyncpg = { version = ">=0.20.0", optional = true }
alibabacloud-devops20210625 = "^5.0.2"
[tool.poetry.group.dev.dependencies]
nonebug = "^0.4"
pytest-cov = "^5.0.0"
pytest-mock = "^3.6.1"
pytest-asyncio = "^0.25"
pytest-xdist = "^3.3.1"
respx = "^0.21.1"
ruff = "^0.8.0"
pre-commit = "^4.0.0"
[tool.poetry.extras]
redis = ["redis"]
postgresql = ["asyncpg"]
[tool.nonebot]
plugins = [
"nonebot_plugin_apscheduler",
"nonebot_plugin_session",
"nonebot_plugin_htmlrender",
"nonebot_plugin_alconna",
]
plugin_dirs = ["zhenxun/services", "zhenxun/builtin_plugins", "zhenxun/plugins"]
adapters = [
{ name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
# { name = "DoDo", module_name = "nonebot.adapters.dodo" },
# { name = "开黑啦", module_name = "nonebot.adapters.kaiheila" },
]
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint]
select = [
"F", # Pyflakes
"W", # pycodestyle warnings
"E", # pycodestyle errors
"I", # isort
"UP", # pyupgrade
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
"T10", # flake8-debugger
"T20", # flake8-print
"PYI", # flake8-pyi
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"TID", # flake8-tidy-imports
"RUF", # Ruff-specific rules
]
ignore = [
"E402", # module-import-not-at-top-of-file
"UP037", # quoted-annotation
"RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring
"RUF003", # ambiguous-unicode-character-comment
"TID252", # relative-imports
]
[tool.ruff.lint.isort]
force-sort-within-sections = true
known-first-party = ["zhenxun", "tests/*"]
extra-standard-library = ["typing_extensions"]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[tool.pyright]
pythonVersion = "3.10"
pythonPlatform = "All"
defineConstant = { PYDANTIC_V2 = true }
executionEnvironments = [
{ root = "./tests", extraPaths = [
"./",
] },
{ root = "./" },
]
typeCheckingMode = "standard"
reportShadowedImports = false
disableBytesTypePromotions = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
-37
View File
@@ -1,37 +0,0 @@
playwright>=1.41.1,<2.0.0
nonebot-adapter-onebot>=2.3.1
nonebot-plugin-apscheduler>=0.5,<0.6
tortoise-orm>=0.20.0,<0.21.0
cattrs>=23.2.3,<24.0.0
ruamel.yaml>=0.18.5,<0.19.0
strenum>=0.4.15,<0.5.0
nonebot-plugin-session>=0.3.2,<0.4.0
ujson>=5.9.0
nb-cli>=1.3.0
nonebot2[fastapi]>=2.3.3
Pillow>=10.0.0,<11.0.0
retrying>=1.3.4,<2.0.0
aiofiles>=23.2.1,<24.0.0
nonebot-plugin-htmlrender>=0.6.0,<1.0.0
pypinyin>=0.51.0
beautifulsoup4>=4.12.3,<5.0.0
lxml>=5.1.0,<6.0.0
psutil>=5.9.8,<6.0.0
feedparser>=6.0.11,<7.0.0
ImageHash>=4.3.1,<5.0.0
cn2an>=0.5.22,<0.6.0
dateparser>=1.2.0,<2.0.0
bilireq>=0.2.10
python-jose[cryptography]>=3.3.0,<4.0.0
python-multipart>=0.0.9,<0.1.0
aiocache[redis]>=0.12.3,<0.13.0
py-cpuinfo>=9.0.0,<10.0.0
nonebot-plugin-alconna>=0.56.0
tenacity>=9.0.0,<10.0.0
nonebot-plugin-uninfo>=0.7.3
nonebot-plugin-waiter>=0.8.1,<0.9.0
multidict>=6.0.0,<7.0.0,!=6.3.2
alibabacloud-devops20210625>=5.0.2,<6.0.0
redis>=5
asyncpg>=0.20.0
-1
View File
@@ -1 +0,0 @@
require_resources_version: ">=1.0.0"
-57
View File
@@ -1,57 +0,0 @@
import asyncio
import os
import re
import nonebot
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
from nonebot.log import logger
nonebot.init()
from zhenxun.services.db_context import disconnect, init
driver = nonebot.get_driver()
driver.register_adapter(OneBotV11Adapter)
driver.on_startup(init)
driver.on_shutdown(disconnect)
# nonebot.load_builtin_plugins("echo")
nonebot.load_plugins("zhenxun/builtin_plugins")
nonebot.load_plugins("zhenxun/plugins")
all_plugins = [name.replace(":", ".") for name in nonebot.get_available_plugin_names()]
logger.info(f"所有插件:{all_plugins}")
loaded_plugins = tuple(
re.sub(r"^zhenxun\.(plugins|builtin_plugins)\.", "", plugin.module_name)
for plugin in nonebot.get_loaded_plugins()
)
logger.info(f"已加载插件:{loaded_plugins}")
for plugin in all_plugins.copy():
if plugin.startswith(("platform",)):
logger.info(f"平台插件:{plugin}")
elif plugin.endswith(loaded_plugins):
logger.info(f"已加载插件:{plugin}")
else:
logger.info(f"未加载插件:{plugin}")
continue
all_plugins.remove(plugin)
if all_plugins:
logger.info(f"出现未加载的插件:{all_plugins}")
exit(1)
else:
logger.info("所有插件均已加载")
@driver.on_startup
async def _():
task = asyncio.create_task(asyncio.sleep(1))
task.add_done_callback(lambda _: driver._lifespan.on_ready(os._exit(0)))
nonebot.run()
View File
@@ -1,421 +0,0 @@
from collections.abc import Callable
import io
import os
from pathlib import Path
import tarfile
from typing import cast
import zipfile
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from respx import MockRouter
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import (
_v11_group_message_event,
_v11_private_message_send,
get_reply_cq,
)
from tests.utils import get_response_json as _get_response_json
def get_response_json(file: str) -> dict:
return _get_response_json(Path() / "auto_update", file)
def init_mocked_api(mocked_api: MockRouter) -> None:
mocked_api.get(
url="https://api.github.com/repos/HibiKier/zhenxun_bot/releases/latest",
name="release_latest",
).respond(json=get_response_json("release_latest.json"))
tar_buffer = io.BytesIO()
zip_bytes = io.BytesIO()
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
# 指定要添加到压缩文件中的文件路径列表
file_paths: list[str] = [
ZhenxunRepoManager.config.PYPROJECT_FILE_STRING,
ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING,
ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING,
]
# 打开一个tarfile对象,写入到上面创建的BytesIO对象中
with tarfile.open(mode="w:gz", fileobj=tar_buffer) as tar:
add_files_and_folders_to_tar(
tar,
file_paths,
folders=ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS,
)
with zipfile.ZipFile(zip_bytes, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf:
add_files_and_folders_to_zip(
zipf,
file_paths,
folders=ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS,
)
mocked_api.get(
url="https://codeload.github.com/HibiKier/zhenxun_bot/legacy.tar.gz/refs/tags/v0.2.2",
name="release_download_url_redirect",
).respond(
content=tar_buffer.getvalue(),
)
mocked_api.get(
url="https://github.com/HibiKier/zhenxun_bot/archive/refs/heads/main.zip",
name="main_download_url",
).respond(
content=zip_bytes.getvalue(),
)
# TODO Rename this here and in `init_mocked_api`
def add_files_and_folders_to_zip(
zipf: zipfile.ZipFile, file_paths: list[str], folders: list[str] = []
):
"""Add files and folders to a zip archive.
This function creates a directory structure within the specified zip
archive and adds the provided files to it. It also creates additional
subdirectories as specified in the folders list.
Args:
zipf: The zip archive to which files and folders will be added.
file_paths: A list of file names to be added to the zip archive.
folders: An optional list of subdirectory names to be created
within the base folder.
"""
# 假设有一个文件夹名为 folder_name
folder_name = "my_folder/"
# 添加文件夹到 ZIP 中,注意 ZIP 中文件夹路径应以 '/' 结尾
zipf.writestr(folder_name, "") # 空内容表示这是一个文件夹
for file_path in file_paths:
# 将文件添加到 ZIP 中,路径为 folder_name + file_name
zipf.writestr(f"{folder_name}{os.path.basename(file_path)}", b"new")
base_folder = f"{folder_name}zhenxun/"
zipf.writestr(base_folder, "")
for folder in folders:
zipf.writestr(f"{base_folder}{folder}/", "")
# TODO Rename this here and in `init_mocked_api`
def add_files_and_folders_to_tar(
tar: tarfile.TarFile, file_paths: list[str], folders: list[str] = []
):
"""Add files and folders to a tar archive.
This function creates a directory structure within the specified tar
archive and adds the provided files to it. It also creates additional
subdirectories as specified in the folders list.
Args:
tar: The tar archive to which files and folders will be added.
file_paths: A list of file names to be added to the tar archive.
folders: An optional list of subdirectory names to be created
within the base folder.
"""
folder_name = "my_folder"
tarinfo = tarfile.TarInfo(folder_name)
add_directory_to_tar(tarinfo, tar)
# 读取并添加指定的文件
for file_path in file_paths:
# 创建TarInfo对象
tar_buffer = io.BytesIO(b"new")
tarinfo = tarfile.TarInfo(
f"{folder_name}/{file_path}"
) # 使用文件名作为tar中的名字
tarinfo.mode = 0o644 # 设置文件夹权限
tarinfo.size = len(tar_buffer.getvalue()) # 设置文件大小
# 添加文件
tar.addfile(tarinfo, fileobj=tar_buffer)
base_folder = f"{folder_name}/zhenxun"
tarinfo = tarfile.TarInfo(base_folder)
add_directory_to_tar(tarinfo, tar)
for folder in folders:
tarinfo = tarfile.TarInfo(f"{base_folder}{folder}")
add_directory_to_tar(tarinfo, tar)
# TODO Rename this here and in `_extracted_from_init_mocked_api_43`
def add_directory_to_tar(tarinfo, tar):
"""Add a directory entry to a tar archive.
This function modifies the provided tarinfo object to set its type
as a directory and assigns the appropriate permissions before adding
it to the specified tar archive.
Args:
tarinfo: The tarinfo object representing the directory.
tar: The tar archive to which the directory will be added.
"""
tarinfo.type = tarfile.DIRTYPE
tarinfo.mode = 0o755
tar.addfile(tarinfo)
def init_mocker_path(mocker: MockerFixture, tmp_path: Path):
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
mocker.patch(
"zhenxun.utils.manager.virtual_env_package_manager.VirtualEnvPackageManager.install_requirement",
return_value=None,
)
mock_tmp_path = mocker.patch(
"zhenxun.configs.path_config.TEMP_PATH",
new=tmp_path / "auto_update",
)
mock_base_path = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_CODE_PATH",
new=tmp_path / "zhenxun",
)
mock_backup_path = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_BACKUP_PATH",
new=tmp_path / "backup",
)
mock_download_gz_file = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_DOWNLOAD_FILE",
new=mock_tmp_path / "download_latest_file.tar.gz",
)
mock_download_zip_file = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_UNZIP_PATH",
new=mock_tmp_path / "download_latest_file.zip",
)
mock_pyproject_file = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.PYPROJECT_FILE",
new=tmp_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING,
)
mock_pyproject_lock_file = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE",
new=tmp_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING,
)
mock_req_txt_file = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.REQUIREMENTS_FILE",
new=tmp_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING,
)
mock_version_file = mocker.patch(
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE",
new=tmp_path / ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE_STRING,
)
open(mock_version_file, "w").write("__version__: v0.2.2")
open(ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE, "w").write(
"__version__: v0.2.2"
)
return (
mock_tmp_path,
mock_base_path,
mock_backup_path,
mock_download_gz_file,
mock_download_zip_file,
mock_pyproject_file,
mock_pyproject_lock_file,
mock_req_txt_file,
mock_version_file,
)
@pytest.mark.xfail
async def test_check_update_release(
app: App,
mocker: MockerFixture,
mocked_api: MockRouter,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试检查更新(release)
"""
from zhenxun.builtin_plugins.auto_update import _matcher
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
init_mocked_api(mocked_api=mocked_api)
(
mock_tmp_path,
mock_base_path,
mock_backup_path,
mock_download_gz_file,
mock_download_zip_file,
mock_pyproject_file,
mock_pyproject_lock_file,
mock_req_txt_file,
mock_version_file,
) = init_mocker_path(mocker, tmp_path)
# 确保目录下有一个子目录,以便 os.listdir() 能返回一个目录名
mock_tmp_path.mkdir(parents=True, exist_ok=True)
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
(mock_base_path / folder).mkdir(parents=True, exist_ok=True)
mock_pyproject_file.write_bytes(b"")
mock_pyproject_lock_file.write_bytes(b"")
mock_req_txt_file.write_bytes(b"")
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot = cast(Bot, bot)
raw_message = "检查更新 release -z"
event = _v11_group_message_event(
raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot, event)
ctx.should_call_send(
event=event,
message=Message(f"{get_reply_cq(MessageId.MESSAGE_ID)}正在进行检查更新..."),
result=None,
bot=bot,
)
ctx.should_call_api(
"send_msg",
_v11_private_message_send(
message="检测真寻已更新,当前版本:v0.2.2\n开始更新...",
user_id=UserId.SUPERUSER,
),
)
ctx.should_call_send(
event=event,
message=Message(
"版本更新完成!\n版本: v0.2.2 -> v0.2.2\n请重新启动真寻以完成更新!"
),
result=None,
bot=bot,
)
ctx.should_finished(_matcher)
assert mocked_api["release_latest"].called
assert mocked_api["release_download_url_redirect"].called
assert (mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING
).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING
).exists()
assert not mock_download_gz_file.exists()
assert not mock_download_zip_file.exists()
assert mock_pyproject_file.read_bytes() == b"new"
assert mock_pyproject_lock_file.read_bytes() == b"new"
assert mock_req_txt_file.read_bytes() == b"new"
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert not (mock_base_path / folder).exists()
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert (mock_backup_path / folder).exists()
@pytest.mark.xfail
async def test_check_update_main(
app: App,
mocker: MockerFixture,
mocked_api: MockRouter,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试检查更新(正式环境)
"""
from zhenxun.builtin_plugins.auto_update import _matcher
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
ZhenxunRepoManager.zhenxun_zip_update = mocker.Mock(return_value="v0.2.2-e6f17c4")
init_mocked_api(mocked_api=mocked_api)
(
mock_tmp_path,
mock_base_path,
mock_backup_path,
mock_download_gz_file,
mock_download_zip_file,
mock_pyproject_file,
mock_pyproject_lock_file,
mock_req_txt_file,
mock_version_file,
) = init_mocker_path(mocker, tmp_path)
# 确保目录下有一个子目录,以便 os.listdir() 能返回一个目录名
mock_tmp_path.mkdir(parents=True, exist_ok=True)
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
(mock_base_path / folder).mkdir(parents=True, exist_ok=True)
mock_pyproject_file.write_bytes(b"")
mock_pyproject_lock_file.write_bytes(b"")
mock_req_txt_file.write_bytes(b"")
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot = cast(Bot, bot)
raw_message = "检查更新 main -r -z"
event = _v11_group_message_event(
raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot, event)
ctx.should_call_send(
event=event,
message=Message(f"{get_reply_cq(MessageId.MESSAGE_ID)}正在进行检查更新..."),
result=None,
bot=bot,
)
ctx.should_call_api(
"send_msg",
_v11_private_message_send(
message="检测真寻已更新,当前版本:v0.2.2\n开始更新...",
user_id=UserId.SUPERUSER,
),
)
ctx.should_call_send(
event=event,
message=Message(
"版本更新完成!\n"
"版本: v0.2.2 -> v0.2.2-e6f17c4\n"
"请重新启动真寻以完成更新!\n"
"真寻资源更新完成!"
),
result=None,
bot=bot,
)
ctx.should_finished(_matcher)
assert mocked_api["main_download_url"].called
assert (mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING
).exists()
assert (
mock_backup_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING
).exists()
assert not mock_download_gz_file.exists()
assert not mock_download_zip_file.exists()
assert mock_pyproject_file.read_bytes() == b"new"
assert mock_pyproject_lock_file.read_bytes() == b"new"
assert mock_req_txt_file.read_bytes() == b"new"
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert (mock_base_path / folder).exists()
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
assert (mock_backup_path / folder).exists()
-202
View File
@@ -1,202 +0,0 @@
from collections import namedtuple
from collections.abc import Callable
from pathlib import Path
import platform
from typing import cast
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import _v11_group_message_event
platform_uname = platform.uname_result(
system="Linux",
node="zhenxun",
release="5.15.0-1027-azure",
version="#1 SMP Debian 5.15.0-1027-azure",
machine="x86_64",
) # type: ignore
cpuinfo_get_cpu_info = {"brand_raw": "Intel(R) Core(TM) i7-10700K"}
def init_mocker(mocker: MockerFixture, tmp_path: Path):
mock_psutil = mocker.patch("zhenxun.builtin_plugins.check.data_source.psutil")
# Define namedtuples for complex return values
CpuFreqs = namedtuple("CpuFreqs", ["current"]) # noqa: PYI024
VirtualMemoryInfo = namedtuple("VirtualMemoryInfo", ["used", "total", "percent"]) # noqa: PYI024
SwapInfo = namedtuple("SwapInfo", ["used", "total", "percent"]) # noqa: PYI024
DiskUsage = namedtuple("DiskUsage", ["used", "total", "free", "percent"]) # noqa: PYI024
# Set specific return values for psutil methods
mock_psutil.cpu_percent.return_value = 1.0 # CPU 使用率
mock_psutil.cpu_freq.return_value = CpuFreqs(current=0.0) # CPU 频率
mock_psutil.cpu_count.return_value = 1 # CPU 核心数
# Memory Info
mock_psutil.virtual_memory.return_value = VirtualMemoryInfo(
used=1 * 1024**3, # 1 GB in bytes for used memory
total=1 * 1024**3, # 1 GB in bytes for total memory
percent=100.0, # 100% of memory used
)
# Swap Info
mock_psutil.swap_memory.return_value = SwapInfo(
used=1 * 1024**3, # 1 GB in bytes for used swap space
total=1 * 1024**3, # 1 GB in bytes for total swap space
percent=100.0, # 100% of swap space used
)
# Disk Usage
mock_psutil.disk_usage.return_value = DiskUsage(
used=1 * 1024**3, # 1 GB in bytes for used disk space
total=1 * 1024**3, # 1 GB in bytes for total disk space
free=0, # No free space
percent=100.0, # 100% of disk space used
)
mock_cpuinfo = mocker.patch("zhenxun.builtin_plugins.check.data_source.cpuinfo")
mock_cpuinfo.get_cpu_info.return_value = cpuinfo_get_cpu_info
mock_platform = mocker.patch("zhenxun.builtin_plugins.check.data_source.platform")
mock_platform.uname.return_value = platform_uname
mock_render_service = mocker.patch(
"zhenxun.builtin_plugins.check.renderer_service.render"
)
mock_render_service_return = mocker.AsyncMock()
mock_render_service.return_value = mock_render_service_return
mock_build_message = mocker.patch(
"zhenxun.builtin_plugins.check.MessageUtils.build_message"
)
mock_build_message_return = mocker.AsyncMock()
mock_build_message.return_value = mock_build_message_return
return (
mock_psutil,
mock_cpuinfo,
mock_platform,
mock_render_service,
mock_render_service_return,
mock_build_message,
mock_build_message_return,
)
@pytest.mark.xfail
async def test_check(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试自检
"""
from zhenxun.builtin_plugins.check import _self_check_matcher
(
mock_psutil,
mock_cpuinfo,
mock_platform,
mock_render_service,
mock_render_service_return,
mock_build_message,
mock_build_message_return,
) = init_mocker(mocker, tmp_path)
async with app.test_matcher(_self_check_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = "自检"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_3,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_ignore_rule(_self_check_matcher)
mock_render_service.assert_awaited_once()
mock_build_message.assert_called_once_with(mock_render_service_return)
mock_build_message_return.send.assert_awaited_once()
@pytest.mark.xfail
async def test_check_arm(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试自检(arm)
"""
from zhenxun.builtin_plugins.check import _self_check_matcher
platform_uname_arm = platform.uname_result(
system="Linux",
node="zhenxun",
release="5.15.0-1017-oracle",
version="#22~20.04.1-Ubuntu SMP Wed Aug 24 11:13:15 UTC 2022",
machine="aarch64",
) # type: ignore
mock_subprocess_check_output = mocker.patch(
"zhenxun.builtin_plugins.check.data_source.subprocess.check_output"
)
mock_environ_copy = mocker.patch(
"zhenxun.builtin_plugins.check.data_source.os.environ.copy"
)
mock_environ_copy_return = mocker.MagicMock()
mock_environ_copy.return_value = mock_environ_copy_return
(
mock_psutil,
mock_cpuinfo,
mock_platform,
mock_render_service,
mock_render_service_return,
mock_build_message,
mock_build_message_return,
) = init_mocker(mocker, tmp_path)
mock_platform.uname.return_value = platform_uname_arm
mock_cpuinfo.get_cpu_info.return_value = {}
mock_psutil.cpu_freq.return_value = {}
async with app.test_matcher(_self_check_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = "自检"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_3,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_ignore_rule(_self_check_matcher)
mock_subprocess_check_output.assert_has_calls(
[
mocker.call(["lscpu"], env=mock_environ_copy_return),
mocker.call().decode(),
mocker.call().decode().splitlines(),
mocker.call().decode().splitlines().__iter__(),
mocker.call(["dmidecode", "-s", "processor-frequency"]),
mocker.call().decode(),
mocker.call().decode().split(),
mocker.call().decode().split().__getitem__(0),
mocker.call().decode().split().__getitem__().__float__(),
] # type: ignore
)
mock_render_service.assert_awaited_once()
mock_build_message.assert_called_once_with(mock_render_service_return)
mock_build_message_return.send.assert_awaited_once()
@@ -1,288 +0,0 @@
from collections.abc import Callable
from pathlib import Path
from typing import cast
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import _v11_group_message_event
test_path = Path(__file__).parent.parent.parent
@pytest.mark.xfail
async def test_add_plugin_basic(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试添加基础插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
plugin_id = "search_image"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"添加插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="插件 识图 安装成功! 重启后生效"),
result=None,
bot=bot,
)
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
@pytest.mark.xfail
async def test_add_plugin_basic_commit_version(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试添加基础插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
plugin_id = "bilibili_sub"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"添加插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="插件 B站订阅 安装成功! 重启后生效"),
result=None,
bot=bot,
)
assert (mock_base_path / "plugins" / "bilibili_sub" / "__init__.py").is_file()
@pytest.mark.xfail
async def test_add_plugin_basic_is_not_dir(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试添加基础插件,插件不是目录
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
plugin_id = "jitang"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"添加插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="插件 鸡汤 安装成功! 重启后生效"),
result=None,
bot=bot,
)
assert (mock_base_path / "plugins" / "jitang.py").is_file()
@pytest.mark.xfail
async def test_add_plugin_extra(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试添加额外插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
plugin_id = "github_sub"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message: str = f"添加插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="插件 github订阅 安装成功! 重启后生效"),
result=None,
bot=bot,
)
assert (mock_base_path / "plugins" / "github_sub" / "__init__.py").is_file()
@pytest.mark.xfail
async def test_plugin_not_exist_add(
app: App,
create_bot: Callable,
) -> None:
"""
测试插件不存在,添加插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
plugin_id = -1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"添加插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在添加插件 Id: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="添加插件 Id: -1 失败 e: 插件ID不存在..."),
result=None,
bot=bot,
)
@pytest.mark.xfail
async def test_add_plugin_exist(
app: App,
mocker: MockerFixture,
create_bot: Callable,
) -> None:
"""
测试插件已经存在,添加插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
return_value=[("search_image", "0.1")],
)
plugin_id = 1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"添加插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在添加插件 Id: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(
message="添加插件 Id: 1 失败 e: 插件 识图 已安装,无需重复安装"
),
result=None,
bot=bot,
)
@@ -1,134 +0,0 @@
# ruff: noqa: ASYNC230
from collections.abc import Callable
from pathlib import Path
from typing import cast
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import _v11_group_message_event
@pytest.mark.xfail
async def test_remove_plugin(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试删除插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
plugin_path = mock_base_path / "plugins" / "search_image"
plugin_path.mkdir(parents=True, exist_ok=True)
with open(plugin_path / "__init__.py", "wb") as f:
f.write(b"A_nmi")
plugin_id = 1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"移除插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message="插件 识图 移除成功! 重启后生效"),
result=None,
bot=bot,
)
assert not (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
@pytest.mark.xfail
async def test_plugin_not_exist_remove(
app: App,
create_bot: Callable,
) -> None:
"""
测试插件不存在,移除插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
plugin_id = -1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"移除插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_2,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message="移除插件 Id: -1 失败 e: 插件ID不存在..."),
result=None,
bot=bot,
)
@pytest.mark.xfail
async def test_remove_plugin_not_install(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试插件未安装,移除插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
_ = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
plugin_id = 1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"移除插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_2,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message="插件 识图 不存在..."),
result=None,
bot=bot,
)
@@ -1,129 +0,0 @@
from collections.abc import Callable
from typing import cast
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import _v11_group_message_event
@pytest.mark.xfail
async def test_search_plugin_name(
app: App,
mocker: MockerFixture,
create_bot: Callable,
) -> None:
"""
测试搜索插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_table_page = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.ImageTemplate.table_page"
)
mock_table_page_return = mocker.AsyncMock()
mock_table_page.return_value = mock_table_page_return
mock_build_message = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.MessageUtils.build_message"
)
mock_build_message_return = mocker.AsyncMock()
mock_build_message.return_value = mock_build_message_return
plugin_name = "github订阅"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"搜索插件 {plugin_name}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_3,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
mock_build_message.assert_called_once_with(mock_table_page_return)
mock_build_message_return.send.assert_awaited_once()
@pytest.mark.xfail
async def test_search_plugin_author(
app: App,
mocker: MockerFixture,
create_bot: Callable,
) -> None:
"""
测试搜索插件,作者
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_table_page = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.ImageTemplate.table_page"
)
mock_table_page_return = mocker.AsyncMock()
mock_table_page.return_value = mock_table_page_return
mock_build_message = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.MessageUtils.build_message"
)
mock_build_message_return = mocker.AsyncMock()
mock_build_message.return_value = mock_build_message_return
author_name = "xuanerwa"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"搜索插件 {author_name}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_3,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
mock_build_message.assert_called_once_with(mock_table_page_return)
mock_build_message_return.send.assert_awaited_once()
@pytest.mark.xfail
async def test_plugin_not_exist_search(
app: App,
create_bot: Callable,
) -> None:
"""
测试插件不存在,搜索插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
plugin_name = "not_exist_plugin_name"
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"搜索插件 {plugin_name}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_3,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message="未找到相关插件..."),
result=None,
bot=bot,
)
@@ -1,112 +0,0 @@
from collections.abc import Callable
from pathlib import Path
from typing import cast
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import _v11_group_message_event
@pytest.mark.xfail
async def test_update_all_plugin_basic_need_update(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试更新基础插件,插件需要更新
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
return_value=[("search_image", "0.0")],
)
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = "更新全部插件"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message="正在更新全部插件"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(
message="--已更新1个插件 0个失败 1个成功--\n* 以下插件更新成功:\n\t- 识图\n重启后生效" # noqa: E501
),
result=None,
bot=bot,
)
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
@pytest.mark.xfail
async def test_update_all_plugin_basic_is_new(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试更新基础插件,插件是最新版
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
return_value=[("search_image", "0.2")],
)
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = "更新全部插件"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message="正在更新全部插件"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="全部插件已是最新版本"),
result=None,
bot=bot,
)
@@ -1,194 +0,0 @@
from collections.abc import Callable
from pathlib import Path
from typing import cast
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot.adapters.onebot.v11.message import Message
from nonebug import App
import pytest
from pytest_mock import MockerFixture
from tests.config import BotId, GroupId, MessageId, UserId
from tests.utils import _v11_group_message_event
@pytest.mark.xfail
async def test_update_plugin_basic_need_update(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试更新基础插件,插件需要更新
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mock_base_path = mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
return_value=[("search_image", "0.0")],
)
plugin_id = 1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"更新插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在更新插件 Id: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="插件 识图 更新成功! 重启后生效"),
result=None,
bot=bot,
)
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
@pytest.mark.xfail
async def test_update_plugin_basic_is_new(
app: App,
mocker: MockerFixture,
create_bot: Callable,
tmp_path: Path,
) -> None:
"""
测试更新基础插件,插件是最新版
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
new=tmp_path / "zhenxun",
)
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
return_value=[("search_image", "0.2")],
)
plugin_id = 1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"更新插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在更新插件 Id: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="插件 识图 已是最新版本"),
result=None,
bot=bot,
)
@pytest.mark.xfail
async def test_plugin_not_exist_update(
app: App,
create_bot: Callable,
) -> None:
"""
测试插件不存在,更新插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
plugin_id = -1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"更新插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_2,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在更新插件 Id: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(message="更新插件 Id: -1 失败 e: 插件ID不存在..."),
result=None,
bot=bot,
)
@pytest.mark.xfail
async def test_update_plugin_not_install(
app: App,
create_bot: Callable,
) -> None:
"""
测试插件不存在,更新插件
"""
from zhenxun.builtin_plugins.plugin_store import _matcher
plugin_id = 1
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
bot: Bot = cast(Bot, bot)
raw_message = f"更新插件 {plugin_id}"
event: GroupMessageEvent = _v11_group_message_event(
message=raw_message,
self_id=BotId.QQ_BOT,
user_id=UserId.SUPERUSER,
group_id=GroupId.GROUP_ID_LEVEL_5,
message_id=MessageId.MESSAGE_ID_2,
to_me=True,
)
ctx.receive_event(bot=bot, event=event)
ctx.should_call_send(
event=event,
message=Message(message=f"正在更新插件 Id: {plugin_id}"),
result=None,
bot=bot,
)
ctx.should_call_send(
event=event,
message=Message(
message="更新插件 Id: 1 失败 e: 插件 识图 未安装,无法更新"
),
result=None,
bot=bot,
)
-22
View File
@@ -1,22 +0,0 @@
class BotId:
QQ_BOT = 12345
class UserId:
SUPERUSER = 10000
SUPERUSER_QQ = 11000
SUPERUSER_DODO = 12000
USER_LEVEL_0 = 10010
USER_LEVEL_5 = 10005
class GroupId:
GROUP_ID_LEVEL_0 = 20000
GROUP_ID_LEVEL_5 = 20005
class MessageId:
MESSAGE_ID = 30001
MESSAGE_ID_2 = 30002
MESSAGE_ID_3 = 30003
MESSAGE_ID_4 = 30004
-144
View File
@@ -1,144 +0,0 @@
from collections.abc import Callable
import json
import os
from pathlib import Path
import nonebot
from nonebug import NONEBOT_INIT_KWARGS
from nonebug.app import App
from nonebug.mixin.process import MatcherContext
import pytest
from pytest_asyncio import is_async_test
from pytest_mock import MockerFixture
from respx import MockRouter
from tests.config import BotId, GroupId, UserId
nonebot.load_plugin("nonebot_plugin_session")
def get_response_json(path: str) -> dict:
return json.loads(
(Path(__file__).parent / "response" / path).read_text(encoding="utf8")
)
def pytest_collection_modifyitems(items: list[pytest.Item]):
pytest_asyncio_tests = (item for item in items if is_async_test(item))
session_scope_marker = pytest.mark.asyncio(loop_scope="session")
for async_test in pytest_asyncio_tests:
async_test.add_marker(session_scope_marker, append=False)
def pytest_configure(config: pytest.Config) -> None:
config.stash[NONEBOT_INIT_KWARGS] = {
"driver": "~fastapi+~httpx+~websockets",
"superusers": [UserId.SUPERUSER.__str__()],
"command_start": [""],
"session_running_expression": "别急呀,小真寻要宕机了!QAQ",
"image_to_bytes": False,
"nickname": ["真寻", "小真寻", "绪山真寻", "小寻子"],
"session_expire_timeout": 30,
"self_nickname": "小真寻",
"db_url": "sqlite://:memory:",
"platform_superusers": {
"qq": [UserId.SUPERUSER_QQ.__str__()],
"dodo": [UserId.SUPERUSER_DODO.__str__()],
},
"host": "127.0.0.1",
"port": 8080,
"log_level": "INFO",
}
@pytest.fixture(scope="session", autouse=True)
def _init_bot(nonebug_init: None):
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
driver = nonebot.get_driver()
driver.register_adapter(OneBotV11Adapter)
nonebot.load_plugin("nonebot_plugin_alconna")
nonebot.load_plugin("nonebot_plugin_apscheduler")
nonebot.load_plugin("nonebot_plugin_htmlrender")
nonebot.load_plugins("zhenxun/builtin_plugins")
nonebot.load_plugins("zhenxun/plugins")
# 手动缓存 uninfo 所需信息
from nonebot_plugin_uninfo import (
Scene,
SceneType,
Session,
SupportAdapter,
SupportScope,
User,
)
from nonebot_plugin_uninfo.adapters.onebot11.main import fetcher as onebot11_fetcher
from nonebot_plugin_uninfo.adapters.onebot12.main import fetcher as onebot12_fetcher
onebot11_fetcher.session_cache = {
f"group_{GroupId.GROUP_ID_LEVEL_5}_{UserId.SUPERUSER}": Session(
self_id="test",
adapter=SupportAdapter.onebot11,
scope=SupportScope.qq_client,
scene=Scene(str(GroupId.GROUP_ID_LEVEL_0), SceneType.GROUP),
user=User(str(UserId.SUPERUSER)),
),
}
onebot12_fetcher.session_cache = {
f"group_{GroupId.GROUP_ID_LEVEL_5}_{UserId.SUPERUSER}": Session(
self_id="test",
adapter=SupportAdapter.onebot12,
scope=SupportScope.qq_client,
scene=Scene(str(GroupId.GROUP_ID_LEVEL_0), SceneType.GROUP),
user=User(str(UserId.SUPERUSER)),
),
}
@pytest.fixture
async def app(app: App, tmp_path: Path, mocker: MockerFixture):
from zhenxun.services.db_context import disconnect, init
driver = nonebot.get_driver()
# 清除连接钩子,现在 NoneBug 会自动触发 on_bot_connect
driver._bot_connection_hook.clear()
mock_config_path = mocker.MagicMock()
mock_config_path.LOG_PATH = tmp_path / "log"
# mock_config_path.LOG_PATH.mkdir(parents=True, exist_ok=True)
mock_config_path.DATA_PATH = tmp_path / "data"
# mock_config_path.DATA_PATH.mkdir(parents=True, exist_ok=True)
mock_config_path.TEMP_PATH = tmp_path / "resources" / "temp"
# mock_config_path.TEMP_PATH.mkdir(parents=True, exist_ok=True)
mocker.patch("zhenxun.configs.path_config", new=mock_config_path)
await init()
# await driver._lifespan.startup()
os.environ["AIOCACHE_DISABLE"] = "1"
os.environ["PYTEST_CURRENT_TEST"] = "1"
yield app
del os.environ["AIOCACHE_DISABLE"]
# await driver._lifespan.shutdown()
await disconnect()
@pytest.fixture
def create_bot() -> Callable:
from nonebot.adapters.onebot.v11 import Adapter, Bot
def _create_bot(context: MatcherContext) -> Bot:
return context.create_bot(
base=Bot,
adapter=nonebot.get_adapter(Adapter),
self_id=BotId.QQ_BOT.__str__(),
)
return _create_bot
@pytest.fixture
def mocked_api(respx_mock: MockRouter):
return respx_mock
@@ -1,39 +0,0 @@
{
"url": "https://api.github.com/repos/HibiKier/zhenxun_bot/releases/172632135",
"assets_url": "https://api.github.com/repos/HibiKier/zhenxun_bot/releases/172632135/assets",
"upload_url": "https://uploads.github.com/repos/HibiKier/zhenxun_bot/releases/172632135/assets{?name,label}",
"html_url": "https://github.com/HibiKier/zhenxun_bot/releases/tag/v0.2.2",
"id": 172632135,
"author": {
"login": "HibiKier",
"id": 45528451,
"node_id": "MDQ6VXNlcjQ1NTI4NDUx",
"avatar_url": "https://avatars.githubusercontent.com/u/45528451?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/HibiKier",
"html_url": "https://github.com/HibiKier",
"followers_url": "https://api.github.com/users/HibiKier/followers",
"following_url": "https://api.github.com/users/HibiKier/following{/other_user}",
"gists_url": "https://api.github.com/users/HibiKier/gists{/gist_id}",
"starred_url": "https://api.github.com/users/HibiKier/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/HibiKier/subscriptions",
"organizations_url": "https://api.github.com/users/HibiKier/orgs",
"repos_url": "https://api.github.com/users/HibiKier/repos",
"events_url": "https://api.github.com/users/HibiKier/events{/privacy}",
"received_events_url": "https://api.github.com/users/HibiKier/received_events",
"type": "User",
"site_admin": false
},
"node_id": "RE_kwDOFe9cjs4KSihH",
"tag_name": "v0.2.2",
"target_commitish": "main",
"name": "v0.2.2",
"draft": false,
"prerelease": false,
"created_at": "2024-08-29T18:48:53Z",
"published_at": "2024-08-29T18:49:34Z",
"assets": [],
"tarball_url": "https://api.github.com/repos/HibiKier/zhenxun_bot/tarball/v0.2.2",
"zipball_url": "https://api.github.com/repos/HibiKier/zhenxun_bot/zipball/v0.2.2",
"body": "* 集成webui\r\n* 移除plugins\r\n* 签到和帮助ui改版\r\n* 修改bug"
}
-71
View File
@@ -1,71 +0,0 @@
import json
from pathlib import Path
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegment
from nonebot.adapters.onebot.v11.event import Sender
def get_reply_cq(uid: int | str) -> str:
return f"[CQ:reply,id={uid}]"
def get_response_json(base_path: Path, file: str) -> dict:
try:
return json.loads(
(Path(__file__).parent / "response" / base_path / file).read_text(
encoding="utf8"
)
)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise ValueError(f"Error reading or parsing JSON file: {e}") from e
def get_content_bytes(base_path: Path, path: str) -> bytes:
try:
return (Path(__file__).parent / "content" / base_path / path).read_bytes()
except FileNotFoundError as e:
raise ValueError(f"Error reading file: {e}") from e
def _v11_group_message_event(
message: str,
self_id: int,
user_id: int,
group_id: int,
message_id: int,
to_me: bool = True,
) -> GroupMessageEvent:
return GroupMessageEvent(
time=1122,
self_id=self_id,
post_type="message",
sub_type="",
user_id=user_id,
message_id=message_id,
message=Message(message),
original_message=Message(message),
message_type="group",
raw_message=message,
font=1,
sender=Sender(user_id=user_id),
to_me=to_me,
group_id=group_id,
)
def _v11_private_message_send(
message: str,
user_id: int,
):
return {
"message_type": "private",
"user_id": user_id,
"message": [
MessageSegment(
type="text",
data={
"text": message,
},
)
],
}
-173
View File
@@ -1,173 +0,0 @@
from datetime import datetime
import uuid
import nonebot
from nonebot.adapters import Bot
from nonebot.drivers import Driver
from tortoise import Tortoise
from tortoise.exceptions import IntegrityError, OperationalError
import ujson as json
from zhenxun.models.bot_connect_log import BotConnectLog
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.goods_info import GoodsInfo
from zhenxun.models.group_member_info import GroupInfoUser
from zhenxun.models.sign_user import SignUser
from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger
from zhenxun.utils.decorator.shop import shop_register
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
from zhenxun.utils.platform import PlatformUtils
driver: Driver = nonebot.get_driver()
@driver.on_bot_connect
async def _(bot: Bot):
logger.debug(f"Bot: {bot.self_id} 建立连接...")
await BotConnectLog.create(
bot_id=bot.self_id, platform=bot.adapter, connect_time=datetime.now(), type=1
)
if not await BotConsole.exists(bot_id=bot.self_id):
try:
await BotConsole.create(
bot_id=bot.self_id, platform=PlatformUtils.get_platform(bot)
)
except IntegrityError as e:
logger.warning(f"记录bot: {bot.self_id} 数据已存在...", e=e)
@driver.on_bot_disconnect
async def _(bot: Bot):
logger.debug(f"Bot: {bot.self_id} 断开连接...")
try:
await BotConnectLog.create(
bot_id=bot.self_id,
platform=bot.adapter,
connect_time=datetime.now(),
type=0,
)
except Exception as e:
logger.warning(f"记录bot: {bot.self_id} 断开连接失败", e=e)
SIGN_SQL = """
SELECT user_id, checkin_count, add_probability, specify_probability, impression
FROM (
SELECT
t1.user_id,
t1.checkin_count,
t1.add_probability,
t1.specify_probability,
t1.impression,
ROW_NUMBER() OVER(PARTITION BY t1.user_id ORDER BY t1.impression DESC) AS rn
FROM sign_group_users t1
INNER JOIN (
SELECT user_id, MAX(impression) AS max_impression
FROM sign_group_users
GROUP BY user_id
) t2 ON t2.user_id = t1.user_id AND t2.max_impression = t1.impression
) t
WHERE rn = 1
"""
BAG_SQL = """
select t1.user_id, t1.gold, t1.property
from bag_users t1
join (
select user_id, max(t2.gold) as max_gold
from bag_users t2
group by user_id
) t on t.user_id = t1.user_id and t.max_gold = t1.gold
"""
@PriorityLifecycle.on_startup(priority=5)
async def _():
if not ZhenxunRepoManager.check_resources_exists():
await ZhenxunRepoManager.resources_update()
"""签到与用户的数据迁移"""
if goods_list := await GoodsInfo.filter(uuid__isnull=True).all():
for goods in goods_list:
goods.uuid = uuid.uuid1() # type: ignore
await GoodsInfo.bulk_update(goods_list, ["uuid"], 10)
await shop_register.load_register()
if (
not await UserConsole.annotate().count()
and not await SignUser.annotate().count()
):
try:
group_user = []
try:
group_user = await GroupInfoUser.filter(uid__isnull=False).all()
except Exception as e:
logger.warning("获取GroupInfoUser数据uid失败...", e=e)
user2uid = {u.user_id: u.uid for u in group_user}
db = Tortoise.get_connection("default")
old_sign_list = await db.execute_query_dict(SIGN_SQL)
old_bag_list = await db.execute_query_dict(BAG_SQL)
goods = {
g["goods_name"]: g["uuid"]
for g in await GoodsInfo.annotate().values("goods_name", "uuid")
}
create_list = []
sign_id_list = []
max_uid = max(user2uid.values()) + 1 if user2uid else 0
for old_sign in old_sign_list:
sign_id_list.append(old_sign["user_id"])
if old_bag := [
b for b in old_bag_list if b["user_id"] == old_sign["user_id"]
]:
old_bag = old_bag[0]
property = json.loads(old_bag["property"])
props = {}
if property:
for name, num in property.items():
if name in goods:
props[goods[name]] = num
create_list.append(
UserConsole(
user_id=old_sign["user_id"],
platform="qq",
uid=user2uid.get(old_sign["user_id"]) or max_uid,
props=props,
gold=old_bag["gold"],
)
)
if not user2uid.get(old_sign["user_id"]):
max_uid += 1
else:
create_list.append(
UserConsole(
user_id=old_sign["user_id"], platform="qq", uid=max_uid
)
)
max_uid += 1
if create_list:
logger.info("开始迁移用户数据...")
await UserConsole.bulk_create(create_list, 10)
logger.info("迁移用户数据完成!")
create_list.clear()
uc_dict = {u.user_id: u for u in await UserConsole.all()}
for old_sign in old_sign_list:
user_console = uc_dict.get(
old_sign["user_id"]
) or await UserConsole.get_user(old_sign["user_id"], "qq")
create_list.append(
SignUser(
user_id=old_sign["user_id"],
user_console=user_console,
platform="qq",
sign_count=old_sign["checkin_count"],
impression=old_sign["impression"],
add_probability=old_sign["add_probability"],
specify_probability=old_sign["specify_probability"],
)
)
if create_list:
logger.info("开始迁移签到数据...")
await SignUser.bulk_create(create_list, 10)
logger.info("迁移签到数据完成!")
except OperationalError as e:
logger.warning("数据迁移", e=e)
-60
View File
@@ -1,60 +0,0 @@
from pathlib import Path
import aiofiles
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
__plugin_meta__ = PluginMetadata(
name="关于",
description="想要更加了解真寻吗",
usage="""
指令:
关于
""".strip(),
extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").to_dict(),
)
_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me())
QQ_INFO = """
『绪山真寻Bot』
版本:{version}
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
""".strip()
INFO = """
『绪山真寻Bot』
版本:{version}
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
项目地址:https://github.com/zhenxun-org/zhenxun_bot
文档地址:https://zhenxun-org.github.io/zhenxun_bot/
""".strip()
@_matcher.handle()
async def _(session: Uninfo, arparma: Arparma):
ver_file = Path() / "__version__"
version = None
if ver_file.exists():
async with aiofiles.open(ver_file, encoding="utf8") as f:
if text := await f.read():
version = text.split(":")[-1].strip()
if PlatformUtils.is_qbot(session):
result: list[str | Path] = [QQ_INFO.format(version=version)]
path = DATA_PATH / "about.png"
if path.exists():
result.append(path)
await MessageUtils.build_message(result).send() # type: ignore
else:
await MessageUtils.build_message(INFO.format(version=version)).send()
logger.info("查看关于", arparma.header_result, session=session)
@@ -1,5 +0,0 @@
from pathlib import Path
import nonebot
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
@@ -1,60 +0,0 @@
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_session import EventSession
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.help_service import create_plugin_help_image
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.exception import EmptyError
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import admin_check, ensure_group
__plugin_meta__ = PluginMetadata(
name="群组管理员帮助",
description="管理员帮助列表",
usage="""
管理员帮助
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.ADMIN,
admin_level=1,
introduction="""这是 群主/群管理 的帮助列表,里面记录了群组内开关功能的
方法帮助以及群管特权方法,建议首次时在群组中发送 '管理员帮助' 查看""",
precautions=[
"只有群主/群管理 才能使用哦,群主拥有6级权限,管理员拥有5级权限!"
],
configs=[],
).to_dict(),
)
async def build_html_help() -> bytes:
"""构建管理员帮助图片"""
return await create_plugin_help_image(
plugin_types=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN],
page_title="群管理员帮助手册",
)
_matcher = on_alconna(
Alconna("管理员帮助"),
rule=admin_check(1) & ensure_group,
priority=5,
block=True,
)
@_matcher.handle()
async def _(
session: EventSession,
arparma: Arparma,
):
try:
image_bytes = await build_html_help()
await MessageUtils.build_message(image_bytes).send()
except EmptyError:
await MessageUtils.build_message("当前管理员帮助为空...").finish(reply_to=True)
logger.info("查看管理员帮助", arparma.header_result, session=session)
@@ -1,67 +0,0 @@
from nonebot import on_notice
from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent
from nonebot.plugin import PluginMetadata
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.models.level_user import LevelUser
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.rules import notice_rule
__plugin_meta__ = PluginMetadata(
name="群管理员变动监测",
description="""检测群管理员变动, 添加与删除管理员默认权限,
当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限""",
usage="",
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.HIDDEN,
configs=[
RegisterConfig(
module="admin_bot_manage",
key="ADMIN_DEFAULT_AUTH",
value=5,
help="设置群欢迎消息所需要的管理员权限等级",
default_value=5,
)
],
).to_dict(),
)
admin_notice = on_notice(priority=5, rule=notice_rule(GroupAdminNoticeEvent))
base_config = Config.get("admin_bot_manage")
@admin_notice.handle()
async def _(event: GroupAdminNoticeEvent):
if event.sub_type == "set":
admin_default_auth = base_config.get("ADMIN_DEFAULT_AUTH")
if admin_default_auth is not None:
await LevelUser.set_level(
str(event.user_id),
str(event.group_id),
admin_default_auth,
)
logger.info(
f"成为管理员,添加权限: {admin_default_auth}",
"群管理员变动监测",
session=event.user_id,
group_id=event.group_id,
)
else:
logger.warning(
"配置项 MODULE: [<u><y>admin_bot_manage</y></u>] |"
" KEY: [<u><y>ADMIN_DEFAULT_AUTH</y></u>] 为空"
)
elif event.sub_type == "unset":
await LevelUser.delete_level(str(event.user_id), str(event.group_id))
logger.info(
"撤销群管理员, 取消权限等级",
"群管理员变动监测",
session=event.user_id,
group_id=event.group_id,
)
@@ -1,334 +0,0 @@
from arclet.alconna import Args
from nonebot.adapters import Bot
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import (
Alconna,
Arparma,
At,
Match,
Option,
on_alconna,
store_true,
)
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.utils import (
AICallableParam,
AICallableProperties,
AICallableTag,
PluginExtraData,
RegisterConfig,
)
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import admin_check
from ._data_source import BanManage, call_ban
base_config = Config.get("ban")
__plugin_meta__ = PluginMetadata(
name="Ban",
description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息",
usage="""
普通管理员
格式:
ban [At用户] ?[-t [时长(分钟)]] ?[-r [理由]]
示例:
ban @用户 : 永久拉黑用户
ban @用户 -t 100 : 拉黑用户100分钟
ban @用户 -t 10 -r 坏 : 拉黑用户10分钟并携带理由
unban @用户 : 从小黑屋中拉出来
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.SUPER_AND_ADMIN,
superuser_help="""
超级管理员额外命令
格式:
ban [At用户/用户Id] ?[-t [时长]] ?[-r [理由]]
unban --id [idx] : 通过id来进行unban操作
ban列表: 获取所有Ban数据
群组ban列表: 获取群组Ban数据
用户ban列表: 获取用户Ban数据
ban列表 -u [用户Id]: 查找指定用户ban数据
ban列表 -g [群组Id]: 查找指定群组ban数据
示例:
ban列表 -u 123456789 : 查找用户123456789的ban数据
ban列表 -g 123456789 : 查找群组123456789的ban数据
私聊下:
示例:
ban 123456789 : 永久拉黑用户123456789
ban 123456789 -r 坏 : 永久拉黑用户123456789并携带理由
ban 123456789 -t 100 : 拉黑用户123456789 100分钟
ban -g 999999 : 拉黑群组为999999的群组
ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟
ban -g 999999 -r 坏 : 永久拉黑群组为999999的群组并携带理由
unban 123456789 : 从小黑屋中拉出来
unban -g 999999 : 将群组9999999从小黑屋中拉出来
""",
admin_level=base_config.get("BAN_LEVEL", 5),
configs=[
RegisterConfig(
key="BAN_LEVEL",
value=5,
help="ban/unban所需要的管理员权限等级",
default_value=5,
type=int,
)
],
smart_tools=[
AICallableTag(
name="call_ban",
description="如果你讨厌某个人(好感度过低并让你感到困扰,或者多次辱骂你),调用此方法进行封禁,调用该方法后要告知用户被封禁和原因",
parameters=AICallableParam(
type="object",
properties={
"user_id": AICallableProperties(
type="string", description="用户的id"
),
"reason": AICallableProperties(
type="string", description="封禁理由"
),
"duration": AICallableProperties(
type="integer",
description="封禁时长(选择的值只能是1-360),单位为分钟,如果频繁触发,按情况增加",
),
},
required=["user_id"],
),
func=call_ban,
)
],
).to_dict(),
)
_ban_matcher = on_alconna(
Alconna(
"ban",
Args["user?", [str, At]],
Option("-r|--reason", Args["reason", str]),
Option("-g|--group", Args["group_id", str]),
Option("-t|--time", Args["duration", int]),
),
rule=admin_check("ban", "BAN_LEVEL"),
priority=5,
block=True,
)
_unban_matcher = on_alconna(
Alconna(
"unban",
Args["user?", [str, At]],
Option("-g|--group", Args["group_id", str]),
Option("--id", Args["idx", int]),
),
rule=admin_check("ban", "BAN_LEVEL"),
priority=5,
block=True,
)
_status_matcher = on_alconna(
Alconna(
"ban列表",
Option("-u", Args["user_id", str], help_text="查找用户"),
Option("-g", Args["group_id", str], help_text="查找群组"),
Option("--user", action=store_true, help_text="过滤用户"),
Option("--group", action=store_true, help_text="过滤群组"),
),
permission=SUPERUSER,
priority=1,
block=True,
)
_status_matcher.shortcut(
"用户ban列表",
command="ban列表",
arguments=["--user"],
prefix=True,
)
_status_matcher.shortcut(
"群组ban列表",
command="ban列表",
arguments=["--group"],
prefix=True,
)
@_status_matcher.handle()
async def _(
arparma: Arparma,
user_id: Match[str],
group_id: Match[str],
):
filter_type = None
if arparma.find("user"):
filter_type = "user"
if arparma.find("group"):
filter_type = "group"
_user_id = user_id.result if user_id.available else None
_group_id = group_id.result if group_id.available else None
if image := await BanManage.build_ban_image(filter_type, _user_id, _group_id):
await MessageUtils.build_message(image).finish(reply_to=True)
else:
await MessageUtils.build_message("数据为空捏...").finish(reply_to=True)
@_ban_matcher.handle()
async def _(
bot: Bot,
session: EventSession,
arparma: Arparma,
user: Match[str | At],
reason: Match[str],
duration: Match[int],
group_id: Match[str],
):
user_id = ""
if not session.id1:
await MessageUtils.build_message("用户id为空...").finish(reply_to=True)
if user.available:
if isinstance(user.result, At):
user_id = user.result.target
else:
if session.id1 not in bot.config.superusers:
await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
user_id = user.result
_duration = duration.result * 60 if duration.available else -1
_duration_text = f"{duration.result} 分钟" if duration.available else " 到世界湮灭"
ban_reason = reason.result if reason.available else None
if (gid := session.id3 or session.id2) and not group_id.available:
if not user_id or (
user_id == bot.self_id and session.id1 not in bot.config.superusers
):
_duration = 0.5
await MessageUtils.build_message("倒反天罡,小小管理速速退下!").send()
await BanManage.ban(session.id1, gid, ban_reason, 30, session, True)
_duration_text = "半 分钟"
logger.info(
f"尝试ban {BotConfig.self_nickname} 反被拿下",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(
[
"对 ",
At(flag="user", target=session.id1),
" 狠狠惩戒了一番,一脚踢进了小黑屋!"
f" 在里面乖乖呆 {_duration_text}吧!",
]
).finish(reply_to=True)
await BanManage.ban(
user_id,
gid,
ban_reason,
_duration,
session,
session.id1 in bot.config.superusers,
)
logger.info(
"管理员Ban",
arparma.header_result,
session=session,
target=f"{gid}:{user_id}",
)
await MessageUtils.build_message(
[
"对 ",
(
At(flag="user", target=user_id)
if isinstance(user.result, At)
else user_id
), # type: ignore
" 狠狠惩戒了一番,一脚踢进了小黑屋!",
f" 在里面乖乖呆 {_duration_text} 吧!",
]
).finish(reply_to=True)
elif session.id1 in bot.config.superusers:
_group_id = group_id.result if group_id.available else None
await BanManage.ban(user_id, _group_id, ban_reason, _duration, session, True)
logger.info(
"超级用户Ban",
arparma.header_result,
session=session,
target=f"{_group_id}:{user_id}",
)
at_msg = user_id or f"群组:{_group_id}"
await MessageUtils.build_message(
f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!"
).finish(reply_to=True)
@_unban_matcher.handle()
async def _(
bot: Bot,
session: EventSession,
arparma: Arparma,
user: Match[str | At],
group_id: Match[str],
idx: Match[int],
):
user_id = ""
_idx = idx.result if idx.available else None
if user.available:
if isinstance(user.result, At):
user_id = user.result.target
else:
if session.id1 not in bot.config.superusers:
await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
user_id = user.result
if gid := session.id3 or session.id2:
if group_id.available:
gid = group_id.result
is_unban, result = await BanManage.unban(
user_id, gid, session, _idx, session.id1 in bot.config.superusers
)
if not is_unban:
await MessageUtils.build_message(result).finish(reply_to=True)
logger.info(
"管理员UnBan",
arparma.header_result,
session=session,
target=f"{gid}:{result}",
)
await MessageUtils.build_message(
[
"将 ",
(
At(flag="user", target=user_id)
if isinstance(user.result, At)
else result
),
" 从黑屋中拉了出来并急救了一下!",
]
).finish(reply_to=True)
elif session.id1 in bot.config.superusers:
_group_id = group_id.result if group_id.available else None
is_unban, result = await BanManage.unban(
user_id, _group_id, session, _idx, True
)
if not is_unban:
await MessageUtils.build_message(result).finish(reply_to=True)
logger.info(
"超级用户UnBan",
arparma.header_result,
session=session,
target=f"{_group_id}:{user_id}",
)
at_msg = user_id or f"群组:{result}"
await MessageUtils.build_message(
f"对 {at_msg} 从黑屋中拉了出来并急救了一下!"
).finish(reply_to=True)
@@ -1,158 +0,0 @@
import time
from typing import Literal
from nonebot_plugin_session import EventSession
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.level_user import LevelUser
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
async def call_ban(user_id: str, reason: str | None = None, duration: int = 1):
"""调用ban
参数:
user_id: 用户id
"""
await BanConsole.ban(user_id, None, 9, reason, duration * 60)
logger.info("被讨厌了,已将用户加入黑名单...", "ban", session=user_id)
class BanManage:
@classmethod
async def build_ban_image(
cls,
filter_type: Literal["group", "user"] | None,
user_id: str | None = None,
group_id: str | None = None,
) -> BuildImage | None:
"""构造Ban列表图片
参数:
filter_type: 过滤类型
user_id: 用户id
group_id: 群组id
返回:
BuildImage | None: Ban列表图片
"""
data_list = None
query = BanConsole
if user_id:
query = query.filter(user_id=user_id)
elif group_id:
query = query.filter(group_id=group_id)
elif filter_type == "user":
query = query.filter(group_id__isnull=True)
elif filter_type == "group":
query = query.filter(user_id__isnull=True)
data_list = await query.all()
if not data_list:
return None
column_name = [
"ID",
"用户ID",
"群组ID",
"BAN LEVEL",
"封禁原因",
"剩余时长(分钟)",
"操作员ID",
]
row_data = []
for data in data_list:
if data.duration == -1:
duration = "∞"
else:
duration = int((data.ban_time + data.duration - time.time()) / 60)
if isinstance(duration, int) and duration < 0:
await data.delete()
continue
row_data.append(
[
data.id,
data.user_id,
data.group_id,
data.ban_level,
data.ban_reason,
duration,
data.operator,
]
)
return await ImageTemplate.table_page(
"Ban / UnBan 列表", "在黑屋中狠狠调教!", column_name, row_data
)
@classmethod
async def is_ban(cls, user_id: str, group_id: str | None):
"""判断用户是否被ban
参数:
user_id: 用户id
返回:
bool: 是否被ban
"""
return await BanConsole.is_ban(user_id, group_id)
@classmethod
async def unban(
cls,
user_id: str | None,
group_id: str | None,
session: EventSession,
idx: int | None = None,
is_superuser: bool = False,
) -> tuple[bool, str]:
"""unban目标用户
参数:
user_id: 用户id
group_id: 群组id
session: Session
idx: 指定id
is_superuser: 是否为超级用户操作
返回:
tuple[bool, str | Non]: 是否unban成功, 群组/用户id或提示
"""
user_level = 9999
if not is_superuser and user_id and session.id1:
user_level = await LevelUser.get_user_level(session.id1, group_id)
if idx:
ban_data = await BanConsole.get_ban(id=idx)
if not ban_data:
return False, "该用户/群组不在黑名单中捏..."
if ban_data.ban_level > user_level:
return False, "unBan权限等级不足捏..."
await ban_data.delete()
return True, ban_data.user_id or ban_data.group_id
elif await BanConsole.check_ban_level(user_id, group_id, user_level):
await BanConsole.unban(user_id, group_id)
return True, group_id or ""
return False, "该用户/群组不在黑名单中不足捏..."
@classmethod
async def ban(
cls,
user_id: str | None,
group_id: str | None,
reason: str | None,
duration: int,
session: EventSession,
is_superuser: bool,
):
"""ban掉目标用户
参数:
user_id: 用户id
group_id: 群组id
reason: 理由
duration: 时长,秒
session: Session
is_superuser: 是否为超级用户操作
"""
level = 9999
if not is_superuser and user_id and session.id1:
level = await LevelUser.get_user_level(session.id1, group_id)
await BanConsole.ban(user_id, group_id, level, reason, duration, session.id1)
@@ -1,168 +0,0 @@
import asyncio
import random
import nonebot
from nonebot import on_notice
from nonebot.adapters import Bot
from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import BotConfig
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.services.tags import tag_manager
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.rules import admin_check, ensure_group, notice_rule
from ._data_source import MemberUpdateManage
__plugin_meta__ = PluginMetadata(
name="更新群组成员列表",
description="更新群组成员列表",
usage="""
更新群组成员的基本信息
指令:
更新群组成员信息
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.SUPER_AND_ADMIN,
admin_level=1,
).to_dict(),
)
_matcher = on_alconna(
Alconna("更新群组成员信息"),
rule=admin_check(1) & ensure_group,
priority=5,
block=True,
)
_notice = on_notice(priority=1, block=False, rule=notice_rule(GroupIncreaseNoticeEvent))
_update_all_matcher = on_alconna(
Alconna("更新所有群组信息"),
permission=SUPERUSER,
priority=1,
block=True,
)
async def _update_all_groups_task(bot: Bot, session: EventSession):
"""
在后台执行所有群组的更新任务,并向超级用户发送最终报告。
"""
success_count = 0
fail_count = 0
total_count = 0
bot_id = bot.self_id
logger.info(f"Bot {bot_id}: 开始执行所有群组信息更新任务...", "更新所有群组")
try:
group_list, _ = await PlatformUtils.get_group_list(bot)
total_count = len(group_list)
for i, group in enumerate(group_list):
try:
logger.debug(
f"Bot {bot_id}: 正在更新第 {i + 1}/{total_count} 个群组: "
f"{group.group_id}",
"更新所有群组",
)
await MemberUpdateManage.update_group_member(bot, group.group_id)
success_count += 1
except Exception as e:
fail_count += 1
logger.error(
f"Bot {bot_id}: 更新群组 {group.group_id} 信息失败",
"更新所有群组",
e=e,
)
await asyncio.sleep(random.uniform(1.5, 3.0))
except Exception as e:
logger.error(f"Bot {bot_id}: 获取群组列表失败,任务中断", "更新所有群组", e=e)
await PlatformUtils.send_superuser(
bot,
f"Bot {bot_id} 更新所有群组信息任务失败:无法获取群组列表。",
session.id1,
)
return
await tag_manager._invalidate_cache()
summary_message = (
f"🤖 Bot {bot_id} 所有群组信息更新任务完成!\n"
f"总计群组: {total_count}\n"
f"✅ 成功: {success_count}\n"
f"❌ 失败: {fail_count}"
)
logger.info(summary_message.replace("\n", " | "), "更新所有群组")
await PlatformUtils.send_superuser(bot, summary_message, session.id1)
@_update_all_matcher.handle()
async def _(bot: Bot, session: EventSession):
await MessageUtils.build_message(
"已开始在后台更新所有群组信息,过程可能需要几分钟到几十分钟,完成后将私聊通知您。"
).send(reply_to=True)
asyncio.create_task(_update_all_groups_task(bot, session)) # noqa: RUF006
@_matcher.handle()
async def _(bot: Bot, session: EventSession, arparma: Arparma):
if gid := session.id3 or session.id2:
logger.info("更新群组成员信息", arparma.header_result, session=session)
result = await MemberUpdateManage.update_group_member(bot, gid)
await MessageUtils.build_message(result).finish(reply_to=True)
await tag_manager._invalidate_cache()
await MessageUtils.build_message("群组id为空...").send()
@_notice.handle()
async def _(bot: Bot, event: GroupIncreaseNoticeEvent):
if str(event.user_id) == bot.self_id:
await MemberUpdateManage.update_group_member(bot, str(event.group_id))
logger.info(
f"{BotConfig.self_nickname}加入群聊更新群组信息",
"更新群组成员列表",
session=event.user_id,
group_id=event.group_id,
)
await tag_manager._invalidate_cache()
@scheduler.scheduled_job(
"interval",
minutes=5,
)
async def _():
for bot in nonebot.get_bots().values():
if PlatformUtils.get_platform(bot) == "qq":
try:
group_list, _ = await PlatformUtils.get_group_list(bot)
if group_list:
for group in group_list:
try:
await MemberUpdateManage.update_group_member(
bot, group.group_id
)
logger.debug("自动更新群组成员信息成功...")
except Exception as e:
logger.error(
f"Bot: {bot.self_id} 自动更新群组成员信息失败",
target=group.group_id,
e=e,
)
except Exception as e:
logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e)
logger.debug(f"自动 Bot: {bot.self_id} 更新群组成员信息成功...")
await tag_manager._invalidate_cache()
@@ -1,160 +0,0 @@
from datetime import datetime
import re
import nonebot
from nonebot.adapters import Bot
from nonebot_plugin_uninfo import Member, SceneType, get_interface
from zhenxun.configs.config import Config
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.group_member_info import GroupInfoUser
from zhenxun.models.level_user import LevelUser
from zhenxun.services.log import logger
from zhenxun.utils.platform import PlatformUtils
class MemberUpdateManage:
@classmethod
async def __handle_user(
cls,
member: Member,
db_user: list[GroupInfoUser],
group_id: str,
data_list: tuple[list, list, list],
platform: str | None,
):
"""单个成员操作
参数:
member: Member
db_user: db成员数据
group_id: 群组id
data_list: 数据列表
platform: 平台
"""
driver = nonebot.get_driver()
default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
nickname = re.sub(
r"[\x00-\x09\x0b-\x1f\x7f-\x9f]", "", member.nick or member.user.name or ""
)
role = member.role
db_user_uid = [u.user_id for u in db_user]
uid2name = {u.user_id: u.user_name for u in db_user}
if member.id in driver.config.superusers:
await LevelUser.set_level(member.id, group_id, 9)
elif role and default_auth:
if role.id != "MEMBER" and not await LevelUser.is_group_flag(
member.id, group_id
):
if role.id == "OWNER":
await LevelUser.set_level(member.id, group_id, default_auth + 1)
elif role.id == "ADMINISTRATOR":
await LevelUser.set_level(member.id, group_id, default_auth)
if cnt := db_user_uid.count(member.id):
users = [u for u in db_user if u.user_id == member.id]
if cnt > 1:
for u in users[1:]:
data_list[2].append(u.id)
if nickname != uid2name.get(member.id):
user = users[0]
user.user_name = nickname
data_list[1].append(user)
else:
data_list[0].append(
GroupInfoUser(
user_id=member.id,
group_id=group_id,
user_name=nickname,
user_join_time=member.joined_at or datetime.now(),
platform=platform,
)
)
@classmethod
async def update_group_member(cls, bot: Bot, group_id: str) -> str:
"""更新群组成员信息
参数:
bot: Bot
group_id: 群组id
返回:
str: 返回消息
"""
if not group_id:
logger.warning(f"bot: {bot.self_id},group_id为空,无法更新群成员信息...")
return "群组id为空..."
if interface := get_interface(bot):
scenes = await interface.get_scenes()
platform = PlatformUtils.get_platform(bot)
group_list = [s for s in scenes if s.is_group and s.id == group_id]
if not group_list:
logger.warning(
f"bot: {bot.self_id},group_id: {group_id},群组不存在,"
"无法更新群成员信息..."
)
return "更新群组失败,群组不存在..."
members = await interface.get_members(SceneType.GROUP, group_list[0].id)
try:
group_console, _ = await GroupConsole.get_or_create(
group_id=group_id, defaults={"platform": platform}
)
group_console.member_count = len(members)
group_console.group_name = group_list[0].name or ""
await group_console.save(update_fields=["member_count", "group_name"])
logger.debug(
f"已更新群组 {group_id} 的成员总数为 {len(members)}",
"更新群组成员信息",
)
except Exception as e:
logger.error(
f"更新群组 {group_id} 的 GroupConsole 信息失败",
"更新群组成员信息",
e=e,
)
db_user = await GroupInfoUser.filter(group_id=group_id).all()
db_user_uid = [u.user_id for u in db_user]
data_list = ([], [], [])
exist_member_list = []
for member in members:
logger.debug(f"即将更新群组成员: {member}", "更新群组成员信息")
await cls.__handle_user(member, db_user, group_id, data_list, platform)
exist_member_list.append(member.id)
if data_list[0]:
try:
await GroupInfoUser.bulk_create(
data_list[0], 30, ignore_conflicts=True
)
logger.debug(
f"创建用户数据 {len(data_list[0])} 条",
"更新群组成员信息",
target=group_id,
)
except Exception as e:
logger.error("批量创建用户数据失败", "更新群组成员信息", e=e)
if data_list[1]:
await GroupInfoUser.bulk_update(data_list[1], ["user_name"], 30)
logger.debug(
f"更新户数据 {len(data_list[1])} 条",
"更新群组成员信息",
target=group_id,
)
if data_list[2]:
await GroupInfoUser.filter(id__in=data_list[2]).delete()
logger.debug(f"删除重复数据 Ids: {data_list[2]}", "更新群组成员信息")
if delete_member_list := [
uid for uid in db_user_uid if uid not in exist_member_list
]:
await GroupInfoUser.filter(
user_id__in=delete_member_list, group_id=group_id
).delete()
logger.info(
f"删除已退群用户 {len(delete_member_list)} 条",
"更新群组成员信息",
group_id=group_id,
platform="qq",
)
return "群组成员信息更新完成!"
@@ -1,45 +0,0 @@
from nonebot.adapters import Bot
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_session import EventSession
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.rules import admin_check, ensure_group
__plugin_meta__ = PluginMetadata(
name="更新群组列表",
description="更新群组列表",
usage="""
更新群组的基本信息
指令:
更新群组信息
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.SUPER_AND_ADMIN,
admin_level=1,
).to_dict(),
)
_matcher = on_alconna(
Alconna("更新群组信息"),
rule=admin_check(1) & ensure_group,
priority=5,
block=True,
)
@_matcher.handle()
async def _(bot: Bot, session: EventSession, arparma: Arparma):
logger.info("更新群组信息", arparma.header_result, session=session)
try:
await PlatformUtils.update_group(bot)
await MessageUtils.build_message("已经成功更新了群组信息!").send(reply_to=True)
except Exception:
await MessageUtils.build_message("更新群组信息失败!").finish(reply_to=True)
@@ -1,372 +0,0 @@
from nonebot.adapters import Bot
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.enum import BlockType, PluginType
from zhenxun.utils.message import MessageUtils
from ._data_source import PluginManager, build_plugin, build_task
from .command import _group_status_matcher, _status_matcher
base_config = Config.get("plugin_switch")
__plugin_meta__ = PluginMetadata(
name="功能开关",
description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制",
usage="""
普通管理员
格式:
开启/关闭[功能名称] : 开关功能
开启/关闭群被动[被动名称] : 群被动开关
开启/关闭所有插件 : 开启/关闭当前群组所有插件状态
开启/关闭所有群被动 : 开启/关闭当前群组所有群被动
群被动状态 : 查看被动技能开关状态
醒来 : 结束休眠
休息吧 : 群组休眠, 不会再响应命令
示例:
开启签到 : 开启签到
关闭签到 : 关闭签到
开启群被动早晚安 : 关闭被动任务早晚安
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.SUPER_AND_ADMIN,
superuser_help="""
格式:
插件列表
开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id]
开启/关闭插件df[功能名称]: 开启/关闭指定插件进群默认状态
开启/关闭所有插件df: 开启/关闭所有插件进群默认状态
开启/关闭所有插件:
私聊中: 开启/关闭所有插件全局状态
群组中: 开启/关闭当前群组所有插件状态
开启/关闭群被动[name] ?[-g [group_id]]
私聊中: 开启/关闭全局指定的被动状态
群组中: 开启/关闭当前群组指定的被动状态
示例:
关闭群被动早晚安
关闭群被动早晚安 -g 12355555
开启/关闭默认群被动 [被动名称]
私聊下: 开启/关闭群被动默认状态
示例:
关闭默认群被动 早晚安
开启/关闭所有群被动 ?[-g [group_id]]
私聊中: 开启/关闭全局或指定群组被动状态
示例:
开启所有群被动: 开启全局所有被动
开启所有群被动 -g 12345678: 开启群组12345678所有被动
私聊下:
示例:
开启签到 : 全局开启签到
关闭签到 : 全局关闭签到
关闭签到 p : 全局私聊关闭签到
关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启)
""",
admin_level=base_config.get("CHANGE_GROUP_SWITCH_LEVEL", 2),
configs=[
RegisterConfig(
key="CHANGE_GROUP_SWITCH_LEVEL",
value=2,
help="开关群功能权限",
default_value=2,
type=int,
)
],
).to_dict(),
)
@_status_matcher.assign("$main")
async def _(
bot: Bot,
session: Uninfo,
arparma: Arparma,
):
if session.user.id in bot.config.superusers:
image = await build_plugin()
logger.info(
"查看功能列表",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(image).finish(reply_to=True)
else:
await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
@_status_matcher.assign("open")
async def _(
bot: Bot,
session: Uninfo,
arparma: Arparma,
plugin_name: Match[str],
group: Match[str],
task: Query[bool] = AlconnaQuery("task.value", False),
default_status: Query[bool] = AlconnaQuery("default.value", False),
all: Query[bool] = AlconnaQuery("all.value", False),
):
if not all.result and not plugin_name.available:
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
name = plugin_name.result
if session.group:
group_id = session.group.id
"""修改当前群组的数据"""
if task.result:
if all.result:
result = await PluginManager.unblock_group_all_task(group_id)
logger.info("开启所有群组被动", arparma.header_result, session=session)
else:
result = await PluginManager.unblock_group_task(name, group_id)
logger.info(
f"开启群组被动 {name}", arparma.header_result, session=session
)
elif session.user.id in bot.config.superusers and default_status.result:
"""单个插件的进群默认修改"""
result = await PluginManager.set_default_status(name, True)
logger.info(
f"超级用户开启 {name} 功能进群默认开关",
arparma.header_result,
session=session,
)
elif all.result:
"""所有插件"""
result = await PluginManager.set_all_plugin_status(
True, default_status.result, group_id
)
logger.info(
"开启群组中全部功能",
arparma.header_result,
session=session,
)
else:
result = await PluginManager.unblock_group_plugin(name, group_id)
logger.info(f"开启功能 {name}", arparma.header_result, session=session)
await MessageUtils.build_message(result).finish(reply_to=True)
elif session.user.id in bot.config.superusers:
"""私聊"""
group_id = group.result if group.available else None
if all.result:
if task.result:
"""关闭全局或指定群全部被动"""
if group_id:
result = await PluginManager.unblock_group_all_task(group_id)
else:
result = await PluginManager.unblock_global_all_task(
default_status.result
)
else:
result = await PluginManager.set_all_plugin_status(
True, default_status.result, group_id
)
logger.info(
"超级用户开启全部功能全局开关"
f" {f'指定群组: {group_id}' if group_id else ''}",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(result).finish(reply_to=True)
if default_status.result and not task.result:
result = await PluginManager.set_default_status(name, True)
logger.info(
f"超级用户开启 {name} 功能进群默认开关",
arparma.header_result,
session=session,
target=group_id,
)
await MessageUtils.build_message(result).finish(reply_to=True)
if task.result:
split_list = name.split()
if len(split_list) > 1:
name = split_list[0]
group_id = split_list[1]
if group_id:
result = await PluginManager.superuser_task_handle(name, group_id, True)
logger.info(
f"超级用户开启被动技能 {name}",
arparma.header_result,
session=session,
target=group_id,
)
else:
result = await PluginManager.unblock_global_task(
name, default_status.result
)
logger.info(
f"超级用户开启全局被动技能 {name}",
arparma.header_result,
session=session,
)
else:
result = await PluginManager.superuser_unblock(name, None, group_id)
logger.info(
f"超级用户开启功能 {name}",
arparma.header_result,
session=session,
target=group_id,
)
await MessageUtils.build_message(result).finish(reply_to=True)
@_status_matcher.assign("close")
async def _(
bot: Bot,
session: Uninfo,
arparma: Arparma,
plugin_name: Match[str],
block_type: Match[str],
group: Match[str],
task: Query[bool] = AlconnaQuery("task.value", False),
default_status: Query[bool] = AlconnaQuery("default.value", False),
all: Query[bool] = AlconnaQuery("all.value", False),
):
if not all.result and not plugin_name.available:
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
name = plugin_name.result
if session.group:
group_id = session.group.id
"""修改当前群组的数据"""
if task.result:
if all.result:
result = await PluginManager.block_group_all_task(group_id)
logger.info("开启所有群组被动", arparma.header_result, session=session)
else:
result = await PluginManager.block_group_task(name, group_id)
logger.info(
f"关闭群组被动 {name}", arparma.header_result, session=session
)
elif session.user.id in bot.config.superusers and default_status.result:
"""单个插件的进群默认修改"""
result = await PluginManager.set_default_status(name, False)
logger.info(
f"超级用户开启 {name} 功能进群默认开关",
arparma.header_result,
session=session,
)
elif all.result:
"""所有插件"""
result = await PluginManager.set_all_plugin_status(
False, default_status.result, group_id
)
logger.info("关闭群组中全部功能", arparma.header_result, session=session)
else:
result = await PluginManager.block_group_plugin(name, group_id)
logger.info(f"关闭功能 {name}", arparma.header_result, session=session)
await MessageUtils.build_message(result).finish(reply_to=True)
elif session.user.id in bot.config.superusers:
group_id = group.result if group.available else None
if all.result:
if task.result:
"""关闭全局或指定群全部被动"""
if group_id:
result = await PluginManager.block_group_all_task(group_id)
else:
result = await PluginManager.block_global_all_task(
default_status.result
)
else:
result = await PluginManager.set_all_plugin_status(
False, default_status.result, group_id
)
logger.info(
"超级用户关闭全部功能全局开关"
f" {f'指定群组: {group_id}' if group_id else ''}",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(result).finish(reply_to=True)
if default_status.result and not task.result:
result = await PluginManager.set_default_status(name, False)
logger.info(
f"超级用户关闭 {name} 功能进群默认开关",
arparma.header_result,
session=session,
target=group_id,
)
await MessageUtils.build_message(result).finish(reply_to=True)
if task.result:
split_list = name.split()
if len(split_list) > 1:
name = split_list[0]
group_id = split_list[1]
if group_id:
result = await PluginManager.superuser_task_handle(
name, group_id, False
)
logger.info(
f"超级用户关闭被动技能 {name}",
arparma.header_result,
session=session,
target=group_id,
)
else:
result = await PluginManager.block_global_task(
name, default_status.result
)
logger.info(
f"超级用户关闭全局被动技能 {name}",
arparma.header_result,
session=session,
)
else:
_type = BlockType.ALL
if block_type.result in ["p", "private"]:
if block_type.available:
_type = BlockType.PRIVATE
elif block_type.result in ["g", "group"]:
if block_type.available:
_type = BlockType.GROUP
result = await PluginManager.superuser_block(name, _type, group_id)
logger.info(
f"超级用户关闭功能 {name}, 禁用类型: {_type}",
arparma.header_result,
session=session,
target=group_id,
)
await MessageUtils.build_message(result).finish(reply_to=True)
@_group_status_matcher.handle()
async def _(
session: Uninfo,
arparma: Arparma,
status: str,
):
if session.group:
group_id = session.group.id
if status == "sleep":
await PluginManager.sleep(group_id)
logger.info("进行休眠", arparma.header_result, session=session)
await MessageUtils.build_message("那我先睡觉了...").finish()
else:
if await PluginManager.is_wake(group_id):
await MessageUtils.build_message("我还醒着呢!").finish()
await PluginManager.wake(group_id)
logger.info("醒来", arparma.header_result, session=session)
await MessageUtils.build_message("呜..醒来了...").finish()
return MessageUtils.build_message("群组id为空...").send()
@_status_matcher.assign("task")
async def _(
session: Uninfo,
arparma: Arparma,
):
image = await build_task(session.group.id if session.group else None)
if image:
logger.info("查看群被动列表", arparma.header_result, session=session)
await MessageUtils.build_message(image).finish(reply_to=True)
else:
await MessageUtils.build_message("获取群被动任务失败...").finish(reply_to=True)
@@ -1,590 +0,0 @@
from typing import cast
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo
from zhenxun.services.cache import CacheRoot
from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.enum import BlockType, CacheType, PluginType
from zhenxun.utils.exception import GroupInfoNotFound
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
def plugin_row_style(column: str, text: str) -> RowStyle:
"""被动技能文本风格
参数:
column: 表头
text: 文本内容
返回:
RowStyle: RowStyle
"""
style = RowStyle()
if (column == "全局状态" and text == "开启") or (
column != "全局状态" and column == "加载状态" and text == "SUCCESS"
):
style.font_color = "#67C23A"
elif column in {"全局状态", "加载状态"}:
style.font_color = "#F56C6C"
return style
async def build_plugin() -> BuildImage:
column_name = [
"ID",
"模块",
"名称",
"全局状态",
"禁用类型",
"加载状态",
"菜单分类",
"作者",
"版本",
"金币花费",
]
plugin_list = await PluginInfo.filter(plugin_type__not=PluginType.HIDDEN).all()
column_data = [
[
plugin.id,
plugin.module,
plugin.name,
"开启" if plugin.status else "关闭",
plugin.block_type,
"SUCCESS" if plugin.load_status else "ERROR",
plugin.menu_type,
plugin.author,
plugin.version,
plugin.cost_gold,
]
for plugin in plugin_list
]
return await ImageTemplate.table_page(
"Plugin",
"插件状态",
column_name,
column_data,
text_style=plugin_row_style,
)
def task_row_style(column: str, text: str) -> RowStyle:
"""被动技能文本风格
参数:
column: 表头
text: 文本内容
返回:
RowStyle: RowStyle
"""
style = RowStyle()
if column in {"群组状态", "全局状态"}:
style.font_color = "#67C23A" if text == "开启" else "#F56C6C"
return style
async def build_task(group_id: str | None) -> BuildImage:
"""构造被动技能状态图片
参数:
group_id: 群组id
异常:
GroupInfoNotFound: 未找到群组
返回:
BuildImage: 被动技能状态图片
"""
task_list = await TaskInfo.all()
column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"]
group = None
if group_id:
group = await GroupConsole.get_group(group_id=group_id)
if not group:
raise GroupInfoNotFound()
else:
column_name.remove("群组状态")
column_data = []
for task in task_list:
if group:
column_data.append(
[
task.id,
task.module,
task.name,
"开启" if f"<{task.module}," not in group.block_task else "关闭",
"开启" if task.status else "关闭",
task.run_time or "-",
]
)
else:
column_data.append(
[
task.id,
task.module,
task.name,
"开启" if task.status else "关闭",
task.run_time or "-",
]
)
return await ImageTemplate.table_page(
"Task",
"被动技能状态",
column_name,
column_data,
text_style=task_row_style,
)
class PluginManager:
@classmethod
async def set_default_status(cls, plugin_name: str, status: bool) -> str:
"""设置插件进群默认状态
参数:
plugin_name: 插件名称
status: 状态
返回:
str: 返回信息
"""
if plugin_name.isdigit():
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
else:
plugin = await PluginInfo.get_or_none(
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
)
if plugin:
plugin.default_status = status
await plugin.save(update_fields=["default_status"])
status_text = "开启" if status else "关闭"
return f"成功将 {plugin.name} 进群默认状态修改为: {status_text}"
return "没有找到这个功能喔..."
@classmethod
async def set_all_plugin_status(
cls, status: bool, is_default: bool = False, group_id: str | None = None
) -> str:
"""修改所有插件状态
参数:
status: 状态
is_default: 是否进群默认.
group_id: 指定群组id.
返回:
str: 返回信息
"""
if is_default:
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
default_status=status
)
return f"成功将所有功能进群默认状态修改为: {'开启' if status else '关闭'}"
if group_id:
if group := await GroupConsole.get_group(group_id=group_id):
module_list = cast(
list[str],
await PluginInfo.filter(plugin_type=PluginType.NORMAL).values_list(
"module", flat=True
),
)
if status:
# 开启所有功能 - 清空禁用列表
group.block_plugin = ""
else:
# 关闭所有功能 - 将模块列表转换为禁用格式
group.block_plugin = CommonUtils.convert_module_format(module_list)
await group.save(update_fields=["block_plugin"])
return f"成功将此群组所有功能状态修改为: {'开启' if status else '关闭'}"
return "获取群组失败..."
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
status=status, block_type=None if status else BlockType.ALL
)
await CacheRoot.invalidate_cache(CacheType.PLUGINS)
return f"成功将所有功能全局状态修改为: {'开启' if status else '关闭'}"
@classmethod
async def is_wake(cls, group_id: str) -> bool:
"""是否醒来
参数:
group_id: 群组id
返回:
bool: 是否醒来
"""
if c := await GroupConsole.get_group(group_id=group_id):
return c.status
return False
@classmethod
async def sleep(cls, group_id: str):
"""休眠
参数:
group_id: 群组id
"""
group, _ = await GroupConsole.get_or_create(
group_id=group_id, channel_id__isnull=True
)
group.status = False
await group.save(update_fields=["status"])
@classmethod
async def wake(cls, group_id: str):
"""醒来
参数:
group_id: 群组id
"""
group, _ = await GroupConsole.get_or_create(
group_id=group_id, channel_id__isnull=True
)
group.status = True
await group.save(update_fields=["status"])
@classmethod
async def block(cls, module: str):
"""禁用
参数:
module: 模块名
"""
if plugin := await PluginInfo.get_plugin(module=module):
plugin.status = False
await plugin.save(update_fields=["status"])
@classmethod
async def unblock(cls, module: str):
"""启用
参数:
module: 模块名
"""
if plugin := await PluginInfo.get_plugin(module=module):
plugin.status = True
await plugin.save(update_fields=["status"])
@classmethod
async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str:
"""禁用群组插件
参数:
plugin_name: 插件名称
group_id: 群组id
返回:
str: 返回信息
"""
return await cls._change_group_plugin(plugin_name, group_id, False)
@classmethod
async def unblock_group_task(cls, task_name: str, group_id: str) -> str:
"""启用被动技能
参数:
task_name: 被动技能名称
group_id: 群组id
返回:
str: 返回信息
"""
return await cls._change_group_task(task_name, group_id, False)
@classmethod
async def unblock_group_all_task(cls, group_id: str) -> str:
"""启用被动技能
参数:
group_id: 群组id
返回:
str: 返回信息
"""
return await cls._change_group_task("", group_id, False, True)
@classmethod
async def block_group_task(cls, task_name: str, group_id: str) -> str:
"""禁用被动技能
参数:
task_name: 被动技能名称
group_id: 群组id
返回:
str: 返回信息
"""
return await cls._change_group_task(task_name, group_id, True)
@classmethod
async def block_group_all_task(cls, group_id: str) -> str:
"""禁用被动技能
参数:
group_id: 群组id
返回:
str: 返回信息
"""
return await cls._change_group_task("", group_id, True, True)
@classmethod
async def block_global_all_task(cls, is_default: bool) -> str:
"""禁用全局被动技能
返回:
str: 返回信息
"""
if is_default:
await TaskInfo.all().update(default_status=False)
return "已禁用所有被动进群默认状态"
else:
await TaskInfo.all().update(status=False)
return "已全局禁用所有被动状态"
@classmethod
async def block_global_task(cls, name: str, is_default: bool = False) -> str:
"""禁用全局被动技能
参数:
name: 被动技能名称
返回:
str: 返回信息
"""
if is_default:
await TaskInfo.filter(name=name).update(default_status=False)
return f"已禁用被动进群默认状态 {name}"
else:
await TaskInfo.filter(name=name).update(status=False)
return f"已全局禁用被动状态 {name}"
@classmethod
async def unblock_global_all_task(cls, is_default: bool) -> str:
"""开启全局被动技能
参数:
is_default: 是否为默认状态
返回:
str: 返回信息
"""
if is_default:
await TaskInfo.all().update(default_status=True)
return "已开启所有被动进群默认状态"
else:
await TaskInfo.all().update(status=True)
return "已全局开启所有被动状态"
@classmethod
async def unblock_global_task(cls, name: str, is_default: bool = False) -> str:
"""开启全局被动技能
参数:
name: 被动技能名称
is_default: 是否为默认状态
返回:
str: 返回信息
"""
if is_default:
await TaskInfo.filter(name=name).update(default_status=True)
return f"已开启被动进群默认状态 {name}"
else:
await TaskInfo.filter(name=name).update(status=True)
return f"已全局开启被动状态 {name}"
@classmethod
async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str:
"""启用群组插件
参数:
plugin_name: 插件名称
group_id: 群组id
返回:
str: 返回信息
"""
return await cls._change_group_plugin(plugin_name, group_id, True)
@classmethod
async def _change_group_task(
cls, task_name: str, group_id: str, status: bool, is_all: bool = False
) -> str:
"""改变群组被动技能状态
参数:
task_name: 被动技能名称
group_id: 群组Id
status: 状态,为True时是关闭
is_all: 所有群被动
返回:
str: 返回信息
"""
status_str = "关闭" if status else "开启"
if is_all:
module_list = cast(
list[str], await TaskInfo.annotate().values_list("module", flat=True)
)
if module_list:
group, _ = await GroupConsole.get_or_create(
group_id=group_id, channel_id__isnull=True
)
if status:
group.block_task = CommonUtils.convert_module_format(module_list)
else:
# 开启所有模块 - 清空禁用列表
group.block_task = ""
await group.save(update_fields=["block_task"])
return f"已成功{status_str}全部被动技能!"
elif task := await TaskInfo.get_or_none(name=task_name):
if status:
await GroupConsole.set_block_task(group_id, task.module)
elif await GroupConsole.is_superuser_block_task(group_id, task.module):
return f"{status_str} {task_name} 被动技能失败,当前群组该被动已被管理员禁用" # noqa: E501
else:
await GroupConsole.set_unblock_task(group_id, task.module)
return f"已成功{status_str} {task_name} 被动技能!"
return "没有找到这个被动技能喔..."
@classmethod
async def _change_group_plugin(
cls, plugin_name: str, group_id: str, status: bool
) -> str:
"""修改群组插件状态
参数:
plugin_name: 插件名称
group_id: 群组id
status: 插件状态
返回:
str: 返回信息
"""
if plugin_name.isdigit():
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
else:
plugin = await PluginInfo.get_or_none(
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
)
if plugin:
status_str = "开启" if status else "关闭"
if status:
if await GroupConsole.is_normal_block_plugin(group_id, plugin.module):
await GroupConsole.set_unblock_plugin(group_id, plugin.module)
return f"已成功{status_str} {plugin.name} 功能!"
elif not await GroupConsole.is_normal_block_plugin(group_id, plugin.module):
await GroupConsole.set_block_plugin(group_id, plugin.module)
return f"已成功{status_str} {plugin.name} 功能!"
return f"该功能已经{status_str}了喔,不要重复{status_str}..."
return "没有找到这个功能喔..."
@classmethod
async def superuser_task_handle(
cls, task_name: str, group_id: str | None, status: bool
) -> str:
"""超级用户禁用被动技能
参数:
task_name: 被动技能名称
group_id: 群组id
status: 状态
返回:
str: 返回信息
"""
if not (task := await TaskInfo.get_or_none(name=task_name)):
return "没有找到这个功能喔..."
if group_id:
if status:
await GroupConsole.set_unblock_task(group_id, task.module, True)
else:
await GroupConsole.set_block_task(group_id, task.module, True)
status_str = "开启" if status else "关闭"
return f"已成功将群组 {group_id} 被动技能 {task_name} {status_str}!"
return "没有找到这个群组喔..."
@classmethod
async def superuser_block(
cls, plugin_name: str, block_type: BlockType | None, group_id: str | None
) -> str:
"""超级用户禁用插件
参数:
plugin_name: 插件名称
block_type: 禁用类型
group_id: 群组id
返回:
str: 返回信息
"""
if plugin_name.isdigit():
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
else:
plugin = await PluginInfo.get_or_none(
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
)
if plugin:
if group_id:
if not await GroupConsole.is_superuser_block_plugin(
group_id, plugin.module
):
await GroupConsole.set_block_plugin(group_id, plugin.module, True)
return f"已成功关闭群组 {group_id} 的 {plugin_name} 功能!"
return "此群组该功能已被超级用户关闭,不要重复关闭..."
plugin.block_type = block_type
plugin.status = not bool(block_type)
await plugin.save(update_fields=["status", "block_type"])
if not block_type:
return f"已成功将 {plugin.name} 全局启用!"
if block_type == BlockType.ALL:
return f"已成功将 {plugin.name} 全局关闭!"
if block_type == BlockType.GROUP:
return f"已成功将 {plugin.name} 全局群组关闭!"
if block_type == BlockType.PRIVATE:
return f"已成功将 {plugin.name} 全局私聊关闭!"
return "没有找到这个功能喔..."
@classmethod
async def superuser_unblock(
cls, plugin_name: str, block_type: BlockType | None, group_id: str | None
) -> str:
"""超级用户开启插件
参数:
plugin_name: 插件名称
block_type: 禁用类型
group_id: 群组id
返回:
str: 返回信息
"""
if plugin_name.isdigit():
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
else:
plugin = await PluginInfo.get_or_none(
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
)
if plugin:
if group_id:
if await GroupConsole.is_superuser_block_plugin(
group_id, plugin.module
):
await GroupConsole.set_unblock_plugin(group_id, plugin.module, True)
return f"已成功开启群组 {group_id} 的 {plugin_name} 功能!"
return "此群组该功能已被超级用户开启,不要重复开启..."
plugin.block_type = block_type
plugin.status = not bool(block_type)
await plugin.save(update_fields=["status", "block_type"])
if not block_type:
return f"已成功将 {plugin.name} 全局启用!"
if block_type == BlockType.ALL:
return f"已成功将 {plugin.name} 全局开启!"
if block_type == BlockType.GROUP:
return f"已成功将 {plugin.name} 全局群组开启!"
if block_type == BlockType.PRIVATE:
return f"已成功将 {plugin.name} 全局私聊开启!"
return "没有找到这个功能喔..."
@@ -1,189 +0,0 @@
from nonebot.rule import to_me
from nonebot_plugin_alconna import (
Alconna,
Args,
Option,
Subcommand,
on_alconna,
store_true,
)
from zhenxun.utils.rules import admin_check, ensure_group
_status_matcher = on_alconna(
Alconna(
"switch",
Option("-t|--task", action=store_true, help_text="被动技能"),
Option("-df|--default", action=store_true, help_text="进群默认开关"),
Option("--all", action=store_true, help_text="全部插件/被动"),
Option("-g|--group", Args["group?", str], help_text="指定群组"),
Subcommand(
"open",
Args["plugin_name?", [str, int]],
),
Subcommand(
"close",
Args["plugin_name?", [str, int]],
Option(
"-t|--type",
Args["block_type?", ["all", "a", "private", "p", "group", "g"]],
),
),
),
rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL"),
priority=5,
block=True,
)
_group_status_matcher = on_alconna(
Alconna("group-status", Args["status", ["sleep", "wake"]]),
rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL")
& ensure_group
& to_me(),
priority=5,
block=True,
)
_status_matcher.shortcut(
r"插件列表",
command="switch",
arguments=[],
prefix=True,
)
_status_matcher.shortcut(
r"群被动状态",
command="switch",
arguments=["--task"],
prefix=True,
)
_status_matcher.shortcut(
r"开启(所有|全部)默认群被动",
command="switch",
arguments=["open", "--task", "--all", "-df"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭(所有|全部)默认群被动",
command="switch",
arguments=["close", "--task", "--all", "-df"],
prefix=True,
)
_status_matcher.shortcut(
r"开启群被动\s*(?P<name>.+)",
command="switch",
arguments=["open", "{name}", "--task"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭群被动\s*(?P<name>.+)",
command="switch",
arguments=["close", "{name}", "--task"],
prefix=True,
)
_status_matcher.shortcut(
r"开启默认群被动\s*(?P<name>.+)",
command="switch",
arguments=["open", "{name}", "--task", "-df"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭默认群被动\s*(?P<name>.+)",
command="switch",
arguments=["close", "{name}", "--task", "-df"],
prefix=True,
)
_status_matcher.shortcut(
r"开启(所有|全部)群被动",
command="switch",
arguments=["open", "--task", "--all"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭(所有|全部)群被动",
command="switch",
arguments=["close", "--task", "--all"],
prefix=True,
)
_status_matcher.shortcut(
r"开启所有(插件|功能)",
command="switch",
arguments=["open", "s", "--all"],
prefix=True,
)
_status_matcher.shortcut(
r"开启所有(插件|功能)df",
command="switch",
arguments=["open", "s", "-df", "--all"],
prefix=True,
)
_status_matcher.shortcut(
r"开启(插件|功能)df(?P<name>.+)",
command="switch",
arguments=["open", "{name}", "-df"],
prefix=True,
)
_status_matcher.shortcut(
r"开启(?P<name>.+)",
command="switch",
arguments=["open", "{name}"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭所有(插件|功能)",
command="switch",
arguments=["close", "s", "--all"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭所有(插件|功能)df",
command="switch",
arguments=["close", "s", "-df", "--all"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭(插件|功能)df(?P<name>.+)",
command="switch",
arguments=["close", "{name}", "-df"],
prefix=True,
)
_status_matcher.shortcut(
r"关闭(?P<name>.+)",
command="switch",
arguments=["close", "{name}"],
prefix=True,
)
_group_status_matcher.shortcut(
r"醒来",
command="group-status",
arguments=["wake"],
prefix=True,
)
_group_status_matcher.shortcut(
r"休息吧",
command="group-status",
arguments=["sleep"],
prefix=True,
)
@@ -1,132 +0,0 @@
from typing import Annotated
from nonebot import on_command
from nonebot.params import Command
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import (
Alconna,
AlconnaMatcher,
Args,
Arparma,
Field,
Match,
Text,
UniMsg,
on_alconna,
)
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import admin_check, ensure_group
from .data_source import Manager
base_config = Config.get("admin_bot_manage")
__plugin_meta__ = PluginMetadata(
name="自定义群欢迎消息",
description="自定义群欢迎消息",
usage="""
设置群欢迎消息,当消息中包含 -at 时会at入群用户
可以设置多条欢迎消息,包含多条欢迎消息时将随机发送
指令:
设置欢迎消息
查看欢迎消息 ?[id]: 存在id时查看指定欢迎消息内容
删除欢迎消息 [id]
示例:
设置欢迎消息 欢迎新人![图片]
设置欢迎消息 欢迎你 -at
查看欢迎消息
查看欢迎消息 2
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.ADMIN,
admin_level=base_config.get("SET_GROUP_WELCOME_MESSAGE_LEVEL", 2),
configs=[
RegisterConfig(
module="admin_bot_manage",
key="SET_GROUP_WELCOME_MESSAGE_LEVEL",
value=2,
help="设置群欢迎消息所需要的管理员权限等级",
default_value=2,
)
],
).to_dict(),
)
_matcher = on_command(
"设置欢迎消息",
rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL")
& ensure_group,
priority=5,
block=True,
)
_show_matcher = on_alconna(
Alconna("查看欢迎消息", Args["idx?", int]),
rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL")
& ensure_group,
priority=5,
block=True,
)
_del_matcher: type[AlconnaMatcher] = on_alconna(
Alconna(
"删除欢迎消息",
Args[
"idx",
int,
Field(
missing_tips=lambda: "请在命令后跟随指定id!",
unmatch_tips=lambda _: "删除指定id必须为数字!",
),
],
),
skip_for_unmatch=False,
rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL")
& ensure_group,
priority=5,
block=True,
)
@_matcher.handle()
async def _(
session: Uninfo,
message: UniMsg,
command: Annotated[tuple[str, ...], Command()],
):
path = Manager.get_path(session)
if not path:
await MessageUtils.build_message("群组不存在...").finish()
message[0].text = message[0].text.replace(command[0], "").strip()
await Manager.save(path, message)
uni_msg = Text("设置欢迎消息成功: \n") + message
await uni_msg.send()
logger.info(f"设置群欢迎消息成功: {message}", command[0], session=session)
@_show_matcher.handle()
async def _(session: Uninfo, arparma: Arparma, idx: Match[int]):
result = await Manager.get_group_message(
session, idx.result if idx.available else None
)
if not result:
await MessageUtils.build_message("当前还未设置群组欢迎消息哦...").finish()
await MessageUtils.build_message(result).send()
logger.info("查看群组欢迎信息", arparma.header_result, session=session)
@_del_matcher.handle()
async def _(session: Uninfo, arparma: Arparma, idx: int):
result = await Manager.delete_group_message(session, idx)
if not result:
await MessageUtils.build_message("未查找到指定id的群组欢迎消息...").finish()
await MessageUtils.build_message(result).send()
logger.info(f"删除群组欢迎信息: {result}", arparma.header_result, session=session)
@@ -1,273 +0,0 @@
import os
from pathlib import Path
import re
import shutil
import uuid
import nonebot
from nonebot_plugin_alconna import UniMessage, UniMsg
from nonebot_plugin_uninfo import Uninfo
import ujson as json
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils._image_template import ImageTemplate
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils
BASE_PATH = DATA_PATH / "welcome_message"
BASE_PATH.mkdir(parents=True, exist_ok=True)
driver = nonebot.get_driver()
def __migrate():
"""首次数据迁移"""
old_file = DATA_PATH / "custom_welcome_msg" / "custom_welcome_msg.json"
if not old_file.exists():
return
try:
old_data: dict[str, str] = json.load(old_file.open(encoding="utf8"))
for group_id, message in old_data.items():
file = BASE_PATH / "qq" / f"{group_id}" / "text.json"
file.parent.mkdir(parents=True, exist_ok=True)
json.dump(
{
uuid.uuid4(): {
"at": "[at]" in message,
"status": True,
"message": message.replace("[at]", ""),
}
},
file.open("w", encoding="utf8"),
ensure_ascii=False,
indent=4,
)
logger.debug("群欢迎消息数据迁移", group_id=group_id)
shutil.rmtree(old_file.parent.absolute())
except Exception as e:
logger.error("群欢迎消息数据迁移失败...", e=e)
def migrate(path: Path):
"""数据迁移
参数:
path: 路径
"""
__migrate()
text_file = path / "text.json"
if not text_file.exists():
return
with text_file.open(encoding="utf8") as f:
json_data = json.load(f)
new_data = {}
if "at" in json_data:
split_msg = re.split(r"\[image:\d\]", str(json_data["message"]))
data = []
for i in range(len(split_msg)):
msg = split_msg[i]
data.append(
{
"type": "text",
"text": msg,
}
)
image_file = path / f"{i}.png"
if image_file.exists():
data.append(
{
"type": "image",
"path": str(image_file),
}
)
new_data[uuid.uuid4()] = {
"at": json_data.get("at", False),
"status": json_data.get("status", True),
"message": data,
}
with text_file.open("w", encoding="utf8") as f:
json.dump(new_data, f, ensure_ascii=False, indent=4)
@PriorityLifecycle.on_startup(priority=5)
def _():
"""数据迁移
参数:
path: 存储路径
json_data: 存储数据
"""
flag_file = BASE_PATH / "flag.txt"
if flag_file.exists():
return
logger.info("开始迁移群欢迎消息数据...")
base_path = BASE_PATH
path_list = []
for platform in os.listdir(BASE_PATH):
base_path = base_path / platform
for group_id in os.listdir(base_path):
group_path = base_path / group_id
is_channel = False
for file in os.listdir(group_path):
inner_file = group_path / file
if inner_file.is_dir():
path_list.append(inner_file)
is_channel = True
if not is_channel:
path_list.append(group_path)
if path_list:
for path in path_list:
migrate(path)
if not flag_file.exists():
flag_file.touch()
logger.success("迁移群欢迎消息数据完成!", "")
class Manager:
@classmethod
def __get_data(cls, session: Uninfo) -> dict | None:
"""获取存储数据
参数:
session: Uninfo
返回:
dict | None: 欢迎消息数据
"""
if not session.group:
return None
path = cls.get_path(session)
if not path:
return None
file = path / "text.json"
if not file.exists():
return None
with file.open(encoding="utf8") as f:
return json.load(f)
@classmethod
def get_path(cls, session: Uninfo) -> Path | None:
"""根据Session获取存储路径
参数:
session: Uninfo:
返回:
Path: 存储路径
"""
if not session.group:
return None
platform = PlatformUtils.get_platform(session)
path = BASE_PATH / f"{platform}" / f"{session.group.id}"
if session.group.parent:
path = (
BASE_PATH
/ f"{platform}"
/ f"{session.group.parent.id}"
/ f"{session.group.id}"
)
path.mkdir(parents=True, exist_ok=True)
return path
@classmethod
async def save(cls, path: Path, message: UniMsg):
"""保存群欢迎消息
参数:
path: 存储路径
message: 消息内容
"""
file = path / "text.json"
json_data = {}
if file.exists():
with file.open(encoding="utf8") as f:
json_data = json.load(f)
data = []
is_at = False
for msg in message.dump(True):
if msg["type"] == "image":
image_file = path / f"{uuid.uuid4()}.png"
await AsyncHttpx.download_file(msg["url"], image_file)
msg["path"] = str(image_file)
if not is_at and msg["type"] == "text" and "-at" in msg["text"]:
msg["text"] = msg["text"].replace("-at", "", 1).strip()
is_at = True
data.append(msg)
json_data[str(uuid.uuid4())] = {"at": is_at, "status": True, "message": data}
with file.open("w", encoding="utf8") as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
@classmethod
async def get_group_message(
cls, session: Uninfo, idx: int | None
) -> BuildImage | UniMessage | None:
"""获取群欢迎消息
参数:
session: Uninfo
idx: 指定id
返回:
list: 消息内容
"""
if not session.group:
return None
json_data = cls.__get_data(session)
if not json_data:
return None
if idx is not None:
key_list = list(json_data.keys())
if idx < 0 or idx > len(key_list):
return None
return UniMessage().load(json_data[key_list[idx]]["message"])
else:
msg_list = []
for i, uid in enumerate(json_data):
msg_data = json_data[uid]
msg_list.append(
[
i,
"开启" if msg_data["status"] else "关闭",
"是" if msg_data["at"] else "否",
str(UniMessage().load(msg_data["message"])),
]
)
if not msg_list:
return None
column_name = ["ID", "状态", "是否@", "消息"]
return await ImageTemplate.table_page(
"群欢迎消息", session.group.id, column_name, msg_list
)
@classmethod
async def delete_group_message(cls, session: Uninfo, idx: int) -> str | None:
"""获取群欢迎消息
参数:
session: EventSession:
id: 消息ID
返回:
list: 消息内容
"""
path = cls.get_path(session)
json_data = cls.__get_data(session)
if not json_data or not path:
return None
file = path / "text.json"
key_list = list(json_data.keys())
if idx < 0 or idx >= len(key_list):
return None
old_msg = str(UniMessage().load(json_data[key_list[idx]]["message"]))
for msg in json_data[key_list[idx]]["message"]:
if msg["type"] == "image" and msg["path"]:
image_path = Path(msg["path"])
if image_path.exists():
image_path.unlink()
del json_data[key_list[idx]]
with file.open("w", encoding="utf8") as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
return f"删除群组欢迎消息成功!消息内容: {old_msg}"
@@ -1,135 +0,0 @@
from nonebot.adapters import Bot
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
from nonebot_plugin_alconna import (
Alconna,
Args,
Match,
Option,
Query,
on_alconna,
store_true,
)
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from ._data_source import UpdateManager
__plugin_meta__ = PluginMetadata(
name="自动更新",
description="就算是真寻也会成长的",
usage="""
usage:
检查更新真寻最新版本,包括了自动更新
资源文件大小一般在130mb左右,除非必须更新一般仅更新代码文件
指令:
检查更新 [main|release|resource|webui] ?[-r] ?[-f] ?[-z] ?[-t]
main: main分支
release: 最新release
resource: 资源文件
webui: webui文件
-r: 下载资源文件,一般在更新main或release时使用
-f: 强制更新,一般用于更新main时使用(仅git更新时有效)
-s: 更新源,为 git 或 ali(默认使用ali)
-z: 下载zip文件进行更新(仅git有效)
-t: 更新方式,git或download(默认使用git)
git: 使用git pull(推荐)
download: 通过commit hash比较文件后下载更新(仅git有效)
示例:
检查更新 main
检查更新 main -r
检查更新 main -f
检查更新 release -r
检查更新 resource
检查更新 webui
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.SUPERUSER,
).to_dict(),
)
_matcher = on_alconna(
Alconna(
"检查更新",
Args["ver_type?", ["main", "release", "resource", "webui"]],
Option("-r|--resource", action=store_true, help_text="下载资源文件"),
Option("-f|--force", action=store_true, help_text="强制更新"),
Option("-s", Args["source?", ["git", "ali"]], help_text="更新源"),
Option("-z|--zip", action=store_true, help_text="下载zip文件"),
),
priority=1,
block=True,
permission=SUPERUSER,
rule=to_me(),
)
@_matcher.handle()
async def _(
bot: Bot,
session: Uninfo,
ver_type: Match[str],
resource: Query[bool] = Query("resource", False),
force: Query[bool] = Query("force", False),
source: Query[str] = Query("source", "ali"),
zip: Query[bool] = Query("zip", False),
):
result = ""
await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True)
if not ver_type.available:
result += await UpdateManager.check_version()
logger.info("查看当前版本...", "检查更新", session=session)
await MessageUtils.build_message(result).finish()
return
ver_type_str = ver_type.result
source_str = source.result
if ver_type_str in {"main", "release"}:
try:
result += await UpdateManager.update_zhenxun(
bot,
session.user.id,
ver_type_str, # type: ignore
force.result,
source_str, # type: ignore
zip.result,
)
except Exception as e:
logger.error("版本更新失败...", "检查更新", session=session, e=e)
await MessageUtils.build_message(f"更新版本失败...e: {e}").finish()
elif ver_type.result == "webui":
if zip.result:
source_str = None
try:
result += await UpdateManager.update_webui(
source_str, # type: ignore
"test",
True,
)
except Exception as e:
logger.error("WebUI更新失败...", "检查更新", session=session, e=e)
result += "\nWebUI更新错误..."
if resource.result or ver_type.result == "resource":
try:
if zip.result:
source_str = None
result += await UpdateManager.update_resources(
source_str, # type: ignore
"main",
force.result,
)
except Exception as e:
logger.error("资源更新下载失败...", "检查更新", session=session, e=e)
result += "\n资源更新错误..."
if result:
await MessageUtils.build_message(result.strip()).finish()
await MessageUtils.build_message("更新版本失败...").finish()
@@ -1,319 +0,0 @@
import asyncio
from typing import Literal
from nonebot.adapters import Bot
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion, Version
from zhenxun.services.log import logger
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
from zhenxun.utils.manager.zhenxun_repo_manager import (
ZhenxunRepoConfig,
ZhenxunRepoManager,
)
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.repo_utils import RepoFileManager
LOG_COMMAND = "AutoUpdate"
class UpdateManager:
@staticmethod
async def _get_latest_commit_date(owner: str, repo: str, path: str) -> str:
"""获取文件最新 commit 日期"""
api_url = f"https://api.github.com/repos/{owner}/{repo}/commits"
params = {"path": path, "page": 1, "per_page": 1}
try:
data = await AsyncHttpx.get_json(api_url, params=params)
if data and isinstance(data, list) and data[0]:
date_str = data[0]["commit"]["committer"]["date"]
return date_str.split("T")[0]
except Exception as e:
logger.warning(f"获取 {owner}/{repo}/{path} 的 commit 日期失败", e=e)
return "获取失败"
@classmethod
async def check_version(cls) -> str:
"""检查真寻和资源的版本"""
bot_cur_version = cls.__get_version()
release_task = ZhenxunRepoManager.zhenxun_get_latest_releases_data()
dev_version_task = RepoFileManager.get_file_content(
ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "__version__"
)
bot_commit_date_task = cls._get_latest_commit_date(
"HibiKier", "zhenxun_bot", "__version__"
)
res_commit_date_task = cls._get_latest_commit_date(
"zhenxun-org", "zhenxun-bot-resources", "__version__"
)
(
release_data,
dev_version_text,
bot_commit_date,
res_commit_date,
) = await asyncio.gather(
release_task,
dev_version_task,
bot_commit_date_task,
res_commit_date_task,
return_exceptions=True,
)
if isinstance(release_data, dict):
bot_release_version = release_data.get("name", "获取失败")
bot_release_date = release_data.get("created_at", "").split("T")[0]
else:
bot_release_version = "获取失败"
bot_release_date = "获取失败"
logger.warning(f"获取 Bot release 信息失败: {release_data}")
if isinstance(dev_version_text, str):
bot_dev_version = dev_version_text.split(":")[-1].strip()
else:
bot_dev_version = "获取失败"
bot_commit_date = "获取失败"
logger.warning(f"获取 Bot dev 版本信息失败: {dev_version_text}")
bot_update_hint = ""
try:
cur_base_v = bot_cur_version.split("-")[0].lstrip("v")
dev_base_v = bot_dev_version.split("-")[0].lstrip("v")
if Version(cur_base_v) < Version(dev_base_v):
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
elif (
Version(cur_base_v) == Version(dev_base_v)
and bot_cur_version != bot_dev_version
):
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
except (InvalidVersion, TypeError, IndexError):
if bot_cur_version != bot_dev_version and bot_dev_version != "获取失败":
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
bot_update_info = (
f"当前版本: {bot_cur_version}\n"
f"最新开发版: {bot_dev_version} (更新于: {bot_commit_date})\n"
f"最新正式版: {bot_release_version} (发布于: {bot_release_date})"
f"{bot_update_hint}"
)
res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__"
res_cur_version = "未找到"
if res_version_file.exists():
if text := res_version_file.open(encoding="utf8").readline():
res_cur_version = text.split(":")[-1].strip()
res_latest_version = "获取失败"
try:
res_latest_version_text = await RepoFileManager.get_file_content(
ZhenxunRepoConfig.RESOURCE_GITHUB_URL, "__version__"
)
res_latest_version = res_latest_version_text.split(":")[-1].strip()
except Exception as e:
res_commit_date = "获取失败"
logger.warning(f"获取资源版本信息失败: {e}")
res_update_hint = ""
try:
if Version(res_cur_version) < Version(res_latest_version):
res_update_hint = "\n-> 发现新资源版本, 可用 `检查更新 resource` 更新"
except (InvalidVersion, TypeError):
pass
res_update_info = (
f"当前版本: {res_cur_version}\n"
f"最新版本: {res_latest_version} (更新于: {res_commit_date})"
f"{res_update_hint}"
)
return f"『绪山真寻 Bot』\n{bot_update_info}\n\n『真寻资源』\n{res_update_info}"
@classmethod
async def update_webui(
cls,
source: Literal["git", "ali"] | None,
branch: str = "dist",
force: bool = False,
):
"""更新WebUI
参数:
source: 更新源
branch: 分支
force: 是否强制更新
返回:
str: 返回消息
"""
if not source:
await ZhenxunRepoManager.webui_zip_update()
return "WebUI更新完成!"
result = await ZhenxunRepoManager.webui_git_update(
source,
branch=branch,
force=force,
)
if not result.success:
logger.error(f"WebUI更新失败...错误: {result.error_message}", LOG_COMMAND)
return f"WebUI更新失败...错误: {result.error_message}"
return "WebUI更新完成!"
@classmethod
async def update_resources(
cls,
source: Literal["git", "ali"] | None,
branch: str = "main",
force: bool = False,
) -> str:
"""更新资源
参数:
source: 更新源
branch: 分支
force: 是否强制更新
返回:
str: 返回消息
"""
if not source:
await ZhenxunRepoManager.resources_zip_update()
return "真寻资源更新完成!"
result = await ZhenxunRepoManager.resources_git_update(
source,
branch=branch,
force=force,
)
if not result.success:
logger.error(
f"真寻资源更新失败...错误: {result.error_message}", LOG_COMMAND
)
return f"真寻资源更新失败...错误: {result.error_message}"
return "真寻资源更新完成!"
@classmethod
async def update_zhenxun(
cls,
bot: Bot,
user_id: str,
version_type: Literal["main", "release"],
force: bool,
source: Literal["git", "ali"],
zip: bool,
) -> str:
"""更新操作
参数:
bot: Bot
user_id: 用户id
version_type: 更新版本类型
force: 是否强制更新
source: 更新源
zip: 是否下载zip文件
update_type: 更新方式
返回:
str | None: 返回消息
"""
cur_version = cls.__get_version()
await PlatformUtils.send_superuser(
bot,
f"检测真寻已更新,当前版本:{cur_version}\n开始更新...",
user_id,
)
result_message = ""
if zip:
new_version = await ZhenxunRepoManager.zhenxun_zip_update(version_type)
await PlatformUtils.send_superuser(
bot, "真寻更新完成,开始安装依赖...", user_id
)
await VirtualEnvPackageManager.install_requirement(
ZhenxunRepoConfig.REQUIREMENTS_FILE
)
result_message = (
f"版本更新完成!\n版本: {cur_version} -> {new_version}\n"
"请重新启动真寻以完成更新!"
)
else:
result = await ZhenxunRepoManager.zhenxun_git_update(
source,
branch=version_type,
force=force,
)
if not result.success:
logger.error(
f"真寻版本更新失败...错误: {result.error_message}",
LOG_COMMAND,
)
return f"版本更新失败...错误: {result.error_message}"
await PlatformUtils.send_superuser(
bot, "真寻更新完成,开始安装依赖...", user_id
)
await VirtualEnvPackageManager.install_requirement(
ZhenxunRepoConfig.REQUIREMENTS_FILE
)
result_message = (
f"版本更新完成!\n"
f"版本: {cur_version} -> {result.new_version}\n"
f"变更文件个数: {len(result.changed_files)}"
f"{'' if source == 'git' else '(阿里云更新不支持查看变更文件)'}\n"
"请重新启动真寻以完成更新!"
)
resource_warning = ""
if version_type == "main":
try:
spec_content = await RepoFileManager.get_file_content(
ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "resources.spec"
)
required_spec_str = None
for line in spec_content.splitlines():
if line.startswith("require_resources_version:"):
required_spec_str = line.split(":", 1)[1].strip().strip("\"'")
break
if required_spec_str:
res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__"
local_res_version_str = "0.0.0"
if res_version_file.exists():
if text := res_version_file.open(encoding="utf8").readline():
local_res_version_str = text.split(":")[-1].strip()
spec = SpecifierSet(required_spec_str)
local_ver = Version(local_res_version_str)
if not spec.contains(local_ver):
warning_header = (
f"⚠️ **资源版本不兼容!**\n"
f"当前代码需要资源版本: `{required_spec_str}`\n"
f"您当前的资源版本是: `{local_res_version_str}`\n"
"**将自动为您更新资源文件...**"
)
await PlatformUtils.send_superuser(bot, warning_header, user_id)
resource_update_source = None if zip else source
resource_update_result = await cls.update_resources(
source=resource_update_source, force=force
)
resource_warning = (
f"\n\n{warning_header}\n{resource_update_result}"
)
except Exception as e:
logger.warning(f"检查资源版本兼容性时出错: {e}", LOG_COMMAND, e=e)
resource_warning = (
"\n\n⚠️ 检查资源版本兼容性时出错,建议手动运行 `检查更新 resource`"
)
return result_message + resource_warning
@classmethod
def __get_version(cls) -> str:
"""获取当前版本
返回:
str: 当前版本号
"""
_version = "v0.0.0"
if ZhenxunRepoConfig.ZHENXUN_BOT_VERSION_FILE.exists():
if text := ZhenxunRepoConfig.ZHENXUN_BOT_VERSION_FILE.open(
encoding="utf8"
).readline():
_version = text.split(":")[-1].strip()
return _version
-58
View File
@@ -1,58 +0,0 @@
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import BotConfig
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.utils.manager.bot_profile_manager import BotProfileManager
from zhenxun.utils.message import MessageUtils
__plugin_meta__ = PluginMetadata(
name="自我介绍",
description=f"这是{BotConfig.self_nickname}的深情告白",
usage="""
指令:
自我介绍
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
menu_type="其他",
superuser_help="""
在data/bot_profile/bot_id/profile.txt 中编辑BOT自我介绍
在data/bot_profile/bot_id/bot_id.png 中编辑BOT头像
指令:
重载自我介绍
""".strip(),
).to_dict(),
)
_matcher = on_alconna(Alconna("自我介绍"), priority=5, block=True, rule=to_me())
_reload_matcher = on_alconna(
Alconna("重载自我介绍"), priority=1, block=True, permission=SUPERUSER
)
@_matcher.handle()
async def _(session: Uninfo, arparma: Arparma):
file_path = await BotProfileManager.build_bot_profile_image(session.self_id)
if not file_path:
await MessageUtils.build_message(
f"{BotConfig.self_nickname}当前没有自我简介哦"
).finish(reply_to=True)
await MessageUtils.build_message(file_path).send()
logger.info("BOT自我介绍", arparma.header_result, session=session)
@_reload_matcher.handle()
async def _(session: Uninfo, arparma: Arparma):
BotProfileManager.clear_profile_image(session.self_id)
await MessageUtils.build_message(f"重载{BotConfig.self_nickname}自我介绍成功").send(
reply_to=True
)
logger.info("重载BOT自我介绍", arparma.header_result, session=session)
-28
View File
@@ -1,28 +0,0 @@
from typing import Any
from nonebot.adapters import Bot
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
Config.add_plugin_config(
"catchphrase",
"CATCHPHRASE",
"",
help="小真寻的口癖,在文本末尾添加指定文字~",
default_value="",
)
@Bot.on_calling_api
async def handle_api_call(bot: Bot, api: str, data: dict[str, Any]):
if api == "send_msg":
catchphrase = Config.get_config("catchphrase", "CATCHPHRASE")
if catchphrase and (message := data.get("message")):
for i in range(len(message) - 1, -1, -1):
if message[i].type == "text":
message[i].data["text"] += catchphrase
logger.debug(
f"文本: {message[i].data['text']} 添加口癖: {catchphrase}"
)
break
@@ -1,5 +0,0 @@
from pathlib import Path
import nonebot
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
@@ -1,72 +0,0 @@
from nonebot import on_message
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import UniMsg
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.models.chat_history import ChatHistory
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.utils import get_entity_ids
__plugin_meta__ = PluginMetadata(
name="消息存储",
description="消息存储,被动存储群消息",
usage="",
extra=PluginExtraData(
author="HibiKier",
version="0.1",
plugin_type=PluginType.HIDDEN,
configs=[
RegisterConfig(
module="chat_history",
key="FLAG",
value=True,
help="是否开启消息自从存储",
default_value=True,
type=bool,
)
],
).to_dict(),
)
def rule(message: UniMsg) -> bool:
return bool(Config.get_config("chat_history", "FLAG") and message)
chat_history = on_message(rule=rule, priority=1, block=False)
TEMP_LIST = []
@chat_history.handle()
async def _(message: UniMsg, session: Uninfo):
entity = get_entity_ids(session)
TEMP_LIST.append(
ChatHistory(
user_id=entity.user_id,
group_id=entity.group_id,
text=str(message),
plain_text=message.extract_plain_text(),
bot_id=session.self_id,
platform=session.platform,
)
)
@scheduler.scheduled_job(
"interval",
minutes=1,
)
async def _():
try:
message_list = TEMP_LIST.copy()
TEMP_LIST.clear()
if message_list:
await ChatHistory.bulk_create(message_list)
logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务")
except Exception as e:
logger.warning("存储聊天记录失败", "chat_history", e=e)
@@ -1,186 +0,0 @@
from datetime import datetime, timedelta
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import (
Alconna,
Args,
Arparma,
Match,
Option,
Query,
on_alconna,
store_true,
)
from nonebot_plugin_session import EventSession
import pytz
from zhenxun import ui
from zhenxun.configs.config import Config
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.group_member_info import GroupInfoUser
from zhenxun.services import avatar_service
from zhenxun.services.log import logger
from zhenxun.ui.builders import TableBuilder
from zhenxun.ui.models import ImageCell, TextCell
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
__plugin_meta__ = PluginMetadata(
name="消息统计",
description="消息统计查询",
usage="""
格式:
消息排行 ?[type [日,周,月,季,年]] ?[--des]
快捷:
[日,周,月,季,年]消息排行 ?[数量]
示例:
消息排行 : 所有记录排行
日消息排行 : 今日记录排行
周消息排行 : 本周记录排行
月消息排行 : 本月记录排行
季消息排行 : 本季度记录排行
年消息排行 : 本年记录排行
消息排行 周 --des : 逆序周记录排行
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.2",
plugin_type=PluginType.NORMAL,
menu_type="数据统计",
commands=[
Command(command="消息统计"),
Command(command="日消息统计"),
Command(command="周消息排行"),
Command(command="月消息排行"),
Command(command="季消息排行"),
Command(command="年消息排行"),
],
configs=[
RegisterConfig(
module="chat_history",
key="SHOW_QUIT_MEMBER",
value=True,
help="是否在消息排行中显示已退群用户",
default_value=True,
type=bool,
)
],
).to_dict(),
)
_matcher = on_alconna(
Alconna(
"消息排行",
Option("--des", action=store_true, help_text="逆序"),
Args["type?", ["日", "周", "月", "季", "年"]]["count?", int, 10],
),
aliases={"消息统计"},
priority=5,
block=True,
)
_matcher.shortcut(
r"(?P<type>['日', '周', '月', '季', '年'])?消息(排行|统计)\s?(?P<cnt>\d+)?",
command="消息排行",
arguments=["{type}", "{cnt}"],
prefix=True,
)
@_matcher.handle()
async def _(
session: EventSession,
arparma: Arparma,
type: Match[str],
count: Query[int] = Query("count", 10),
):
group_id = session.id3 or session.id2
time_now = datetime.now()
date_scope = None
zero_today = time_now - timedelta(
hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second
)
date = type.result if type.available else None
if date:
if date in ["日"]:
date_scope = (zero_today, time_now)
elif date in ["周"]:
date_scope = (time_now - timedelta(days=7), time_now)
elif date in ["月"]:
date_scope = (time_now - timedelta(days=30), time_now)
elif date in ["季"]:
date_scope = (time_now - timedelta(days=90), time_now)
column_name = ["名次", "头像", "昵称", "发言次数"]
show_quit_member = Config.get_config("chat_history", "SHOW_QUIT_MEMBER", True)
fetch_count = count.result
if not show_quit_member:
fetch_count = count.result * 2
if rank_data := await ChatHistory.get_group_msg_rank(
group_id, fetch_count, "DES" if arparma.find("des") else "DESC", date_scope
):
rows_data = []
platform = "qq"
user_ids_in_rank = [str(uid) for uid, _ in rank_data]
users_in_group_query = GroupInfoUser.filter(
user_id__in=user_ids_in_rank, group_id=group_id
)
users_in_group = {u.user_id: u for u in await users_in_group_query}
for idx, (uid, num) in enumerate(rank_data):
if len(rows_data) >= count.result:
break
uid_str = str(uid)
user_in_group = users_in_group.get(uid_str)
if not user_in_group and not show_quit_member:
continue
user_name = (
user_in_group.user_name if user_in_group else f"{uid_str}(已退群)"
)
avatar_path = await avatar_service.get_avatar_path(platform, uid_str)
rows_data.append(
[
TextCell(content=str(len(rows_data) + 1)),
ImageCell(
src=avatar_path.as_uri() if avatar_path else "", shape="circle"
),
TextCell(content=user_name),
TextCell(content=str(num), bold=True),
]
)
if not date_scope:
first_msg_time = await ChatHistory.get_group_first_msg_datetime(group_id)
if first_msg_time:
date_scope_start = first_msg_time.astimezone(
pytz.timezone("Asia/Shanghai")
).replace(microsecond=0)
date_str = f"{str(date_scope_start).split('+')[0]} - 至今"
else:
date_str = f"{time_now.replace(microsecond=0)} - 至今"
else:
date_str = (
f"{date_scope[0].replace(microsecond=0)} - "
f"{date_scope[1].replace(microsecond=0)}"
)
builder = TableBuilder(f"消息排行({count.result})", date_str)
builder.set_headers(column_name).add_rows(rows_data)
image_bytes = await ui.render(builder.build())
logger.info(
f"查看消息排行 数量={count.result}", arparma.header_result, session=session
)
await MessageUtils.build_message(image_bytes).finish(reply_to=True)
await MessageUtils.build_message("群组消息记录为空...").finish()
-90
View File
@@ -1,90 +0,0 @@
from nonebot import on_notice
from nonebot.adapters.onebot.v11 import PokeNotifyEvent
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot.rule import Rule, to_me
from nonebot_plugin_alconna import Alconna, on_alconna
from zhenxun import ui
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import notice_rule
from .data_source import get_status_info
__plugin_meta__ = PluginMetadata(
name="服务器自我检查",
description="查看服务器当前状态",
usage="""
查看服务器当前状态
指令:
自检
戳一戳BOT
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.2",
plugin_type=PluginType.SUPERUSER,
configs=[
RegisterConfig(
key="type",
value="mix",
help="自检触发方式 ['message', 'poke', 'mix']",
default_value="mix",
)
],
).to_dict(),
)
def commandRule() -> Rule:
return Rule(lambda: Config.get_config("check", "type") in {"message", "mix"})
def noticeRule() -> Rule:
return Rule(lambda: Config.get_config("check", "type") in {"poke", "mix"})
_self_check_matcher = _self_check_matcher = on_alconna(
Alconna("自检"),
rule=to_me() & commandRule(),
permission=SUPERUSER,
block=True,
priority=1,
)
_self_check_poke_matcher = on_notice(
priority=5,
permission=SUPERUSER,
block=False,
rule=notice_rule(PokeNotifyEvent) & to_me() & noticeRule(),
)
async def handle_self_check():
try:
data_dict = await get_status_info()
image_bytes = await ui.render_template(
"pages/builtin/check",
data=data_dict,
)
await MessageUtils.build_message(image_bytes).send()
logger.info("自检成功", "自检")
except Exception as e:
await MessageUtils.build_message(f"自检失败: {e}").send()
logger.error("自检失败", "自检", e=e)
@_self_check_matcher.handle()
async def handle_message_check():
await handle_self_check()
@_self_check_poke_matcher.handle()
async def handle_poke_check():
await handle_self_check()

Some files were not shown because too many files have changed in this diff Show More