Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059b5c3a33 | ||
|
|
024f1e015b | ||
|
|
929bbe5095 | ||
|
|
b06795cfaf | ||
|
|
1b51344332 | ||
|
|
c0abb730b6 | ||
|
|
71e674efc0 | ||
|
|
af0d8415ae | ||
|
|
c30f6b56c6 | ||
|
|
9eda3e4e48 |
@@ -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,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服务器的局部命令,其余命令不注册
|
||||
@@ -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 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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: 我有能力且愿意为这个功能贡献代码
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
MD013: false
|
||||
MD024: # 重复标题
|
||||
siblings_only: true
|
||||
MD033: false # 允许 html
|
||||
@@ -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]
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"charliermarsh.ruff",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) 联系我们。
|
||||
|
||||
再次感谢你的贡献!
|
||||
@@ -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"]
|
||||
@@ -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/>.
|
||||
@@ -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>
|
||||
|
||||
[](https://qm.qq.com/q/mRNtLSl6uc)
|
||||
[](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;">
|
||||
|
||||
#### 登录界面
|
||||
|
||||

|
||||
|
||||
#### API 设置
|
||||
|
||||

|
||||
|
||||
#### 仪表盘
|
||||
|
||||

|
||||
|
||||
#### 仪表盘(展开)
|
||||
|
||||

|
||||
|
||||
#### 控制台
|
||||
|
||||

|
||||
|
||||
#### 插件列表
|
||||
|
||||

|
||||
|
||||
#### 插件列表(配置项)
|
||||
|
||||

|
||||
|
||||
#### 插件商店
|
||||
|
||||

|
||||
|
||||
#### 好友/群组管理
|
||||
|
||||

|
||||
|
||||
#### 请求管理
|
||||
|
||||

|
||||
|
||||
#### 数据库管理
|
||||
|
||||

|
||||
|
||||
### 文件管理
|
||||
|
||||

|
||||
|
||||
### 文件管理(文本查看)
|
||||
|
||||

|
||||
|
||||
### 文件管理(图片查看)
|
||||
|
||||

|
||||
|
||||
### 关于
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
代码和人有一个能run就行了
|
||||
已经坐船到柬埔寨了,马上转船到金三角了(
|
||||
|
||||
@@ -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(
|
||||
@@ -0,0 +1,3 @@
|
||||
## 要不跑路吧
|
||||
代碼和人有一個能run就行了
|
||||
已經坐船到柬埔寨了,馬上轉船到金三角了(
|
||||
@@ -1 +0,0 @@
|
||||
__version__: v0.2.4-da6d5b4
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 799 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 694 KiB |
|
Before Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 708 KiB |
|
Before Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 405 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 917 KiB |
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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' ]
|
||||
@@ -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"
|
||||
@@ -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 +0,0 @@
|
||||
require_resources_version: ">=1.0.0"
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||