├── .github ├── ISSUE_TEMPLATE │ ├── bug报告-bug-report.md │ ├── 功能需求-feature-request.md │ └── 提出问题-ask-a-question.md └── workflows │ ├── docker-build-on-merge.yml │ ├── docker-build-on-release.yml │ └── stale.yml ├── .gitignore ├── Docker ├── Dockerfile ├── README.md ├── docker-compose.yaml └── root │ ├── bin │ └── xybot-start │ ├── payloads.sh │ ├── payloads │ ├── 001-clone-XYBot.sh │ └── 002-install-XYBot-dep.sh │ └── wx-entrypoint.sh ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── index.html ├── xybot_logo_32x32.png └── zh-cn │ ├── XYBotLinux部署.md │ ├── XYBotWindows部署.md │ ├── XYBot功能介绍.md │ ├── XYBot插件编写.md │ ├── XYBot的设置.md │ └── _coverpage.md ├── main_config.yml ├── plans ├── antiautolog.py ├── cache_clear.py ├── daily_greeting.py └── expired_red_packets_check.py ├── plugins ├── command │ ├── _at_test.py │ ├── _blocker.py │ ├── admin_points.py │ ├── admin_points.yml │ ├── admin_signin_reset.py │ ├── admin_signin_reset.yml │ ├── admin_whitelist.py │ ├── admin_whitelist.yml │ ├── at_test.yml │ ├── blocker.yml │ ├── bot_status.py │ ├── bot_status.yml │ ├── dalle3.py │ ├── dalle3.yml │ ├── food_selector.py │ ├── food_selector.yml │ ├── get_contact_list.py │ ├── get_contact_list.yml │ ├── gomoku.py │ ├── gomoku.yml │ ├── gpt.py │ ├── gpt.yml │ ├── hypixel_info.py │ ├── hypixel_info.yml │ ├── lucky_draw.py │ ├── lucky_draw.yml │ ├── manage_plugins.py │ ├── manage_plugins.yml │ ├── menu.py │ ├── menu.yml │ ├── news.py │ ├── news.yml │ ├── points_leaderboard.py │ ├── points_leaderboard.yml │ ├── points_trade.py │ ├── points_trade.yml │ ├── query_points.py │ ├── query_points.yml │ ├── random_picture.py │ ├── random_picture.yml │ ├── red_packet.py │ ├── red_packet.yml │ ├── sign_in.py │ ├── sign_in.yml │ ├── warthunder.py │ ├── warthunder.yml │ ├── weather.py │ └── weather.yml ├── image │ └── _image_test.py ├── join_group │ ├── _join_group_test.py │ └── join_notification.py ├── mention │ ├── _mention_test.py │ ├── mention_gpt.py │ └── mention_gpt.yml ├── text │ ├── private_chatgpt.py │ └── private_chatgpt.yml └── voice │ └── _voice_test.py ├── requirements.txt ├── resources └── gomoku_board_original.png ├── sensitive_words.yml ├── start.py ├── utils ├── database.py ├── plans_interface.py ├── plans_manager.py ├── plugin_interface.py ├── plugin_manager.py ├── singleton.py └── xybot.py └── wcferry_helper ├── __init__.py ├── injector.exe ├── injector ├── go.mod └── main.go ├── sdk.dll ├── spy.dll ├── spy_debug.dll └── wcferry_helper.py /.github/ISSUE_TEMPLATE/bug报告-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug报告 Bug report 3 | about: 创建一个报告来帮助我们改进 。Create a report to help us improve. 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **请描述这个 Bug Describe the bug** 11 | Bug的清晰简洁的描述。 12 | A clear and concise description of what the bug is. 13 | 14 | **如何复现 To Reproduce** 15 | 重现该行为的步骤 Steps to reproduce the behavior: 16 | 1. 发送 "......" Send "..." 17 | 2. 点击 "......" Click "..." 18 | 3. 查看报错 See error 19 | 20 | **预期行为 Expected behavior** 21 | 期望发生的事情。 22 | A clear and concise description of what you expected to happen. 23 | 24 | **截屏 Screenshots** 25 | 如果可以的话,添加截图来帮助解释。 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **运行环境 Operating environment** 29 | - OS: [e.g. Windows10, Windows11, In Docker] 30 | - Python版本 Python version: [e.g. 3.12, In Docker] 31 | - XYBot版本 XYBot version [e.g. 2.0.0] 32 | 33 | **额外信息 Additional context** 34 | 有关该问题的任何其他信息。 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/功能需求-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能需求 Feature request 3 | about: 为这个项目提出一个想法 Suggest an idea for this project 4 | title: "[Enhancement]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **描述一下你的需求或者想法 Describe your needs or ideas** 11 | 清晰简洁地描述一下想法。 12 | A clear and concise description of what the idea is. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/提出问题-ask-a-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提出问题 Ask a question 3 | about: 我想提出一个问题 I want to ask a question. 4 | title: "[Question]" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **你的问题 Your question** 11 | 详细地说明你的问题是什么。 12 | Explain in detail what your problem is. 13 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build on Merge 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v2 18 | 19 | - name: Log in to Docker Hub 20 | uses: docker/login-action@v3 21 | with: 22 | username: ${{ vars.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | 25 | - name: Get short SHA 26 | id: vars 27 | run: echo "GIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 28 | 29 | - name: Build and push Docker image 30 | uses: docker/build-push-action@v4 31 | with: 32 | context: ./Docker 33 | push: true 34 | tags: henryxiaoyang/xybot:latest,henryxiaoyang/xybot:${{ env.GIT_SHA }} -------------------------------------------------------------------------------- /.github/workflows/docker-build-on-release.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build on Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v2 17 | 18 | - name: Log in to Docker Hub 19 | uses: docker/login-action@v3 20 | with: 21 | username: ${{ vars.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | 24 | - name: Build and push Docker image 25 | uses: docker/build-push-action@v4 26 | with: 27 | context: ./Docker 28 | push: true 29 | tags: henryxiaoyang/xybot:latest,henryxiaoyang/xybot:${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '0 12 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | days-before-stale: 30 25 | days-before-close: 7 26 | stale-issue-message: 'This issue has been labeled `Stale` as it had remained inactive for a period of 30 days.' 27 | stale-pr-message: 'This pull request has been labeled `Stale` as it had remained inactive for a period of 30 days.' 28 | stale-issue-label: 'Stale' 29 | stale-pr-label: 'Stale' 30 | close-issue-message: 'This issue has been closed as it had remained inactive for a period of 7 days.' 31 | close-pr-message: 'This pull request has been closed as it had remained inactive for a period of 7 days.' 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | 163 | # XYBot 164 | userdata.db 165 | gomoku_board.psd 166 | 167 | #Github 168 | 169 | debugger.py 170 | pywxdll_test.py 171 | -------------------------------------------------------------------------------- /Docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM henryxiaoyang/wechat-box-3.9.10.27:latest 2 | 3 | # clear /payloads 4 | RUN sudo rm -rf /payloads 5 | 6 | COPY root/ / 7 | 8 | # init with GUI 9 | RUN bash -c 'nohup /entrypoint.sh 2>&1 &' && sleep 5 && /payloads.sh \ 10 | && sudo cp -r /wechat-etc/* /etc/ \ 11 | && sudo rm /tmp/.X0-lock 12 | #settings 13 | 14 | ENTRYPOINT ["/wx-entrypoint.sh"] -------------------------------------------------------------------------------- /Docker/README.md: -------------------------------------------------------------------------------- 1 | ## 如何使用: 2 | 3 | 请勿使用`root`用户,权限会有问题 4 | 5 | 1. ```sudo docker build -t henryxiaoyang/xybot:latest -t henryxiaoyang/xybot:v2.0.0 .``` 6 | 2. docker-compose up -------------------------------------------------------------------------------- /Docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | xybot: 5 | image: "henryxiaoyang/xybot:v2.0.0" 6 | restart: unless-stopped 7 | container_name: "XYBot" 8 | environment: 9 | WC_AUTO_RESTART: "yes" 10 | ports: 11 | - "4000:8080" 12 | extra_hosts: 13 | - "dldir1.qq.com:127.0.0.1" 14 | volumes: 15 | - "XYBot:/home/app/XYBot/" 16 | - "XYBot-wechatfiles:/home/app/WeChat Files/" 17 | tty: true 18 | 19 | volumes: 20 | XYBot: 21 | XYBot-wechatfiles: -------------------------------------------------------------------------------- /Docker/root/bin/xybot-start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function start_xybot() { 4 | while : 5 | do 6 | python3 start.py 7 | sleep 10 8 | done 9 | } 10 | 11 | cd /home/app/XYBot || exit 12 | start_xybot & 13 | wait -------------------------------------------------------------------------------- /Docker/root/payloads.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | for i in `ls /payloads/*|sort` 4 | do 5 | [ -x "$i" ] && { 6 | "$i" 7 | } 8 | done -------------------------------------------------------------------------------- /Docker/root/payloads/001-clone-XYBot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd /home/app || exit 4 | git clone https://github.com/HenryXiaoYang/XYBot.git 5 | cd XYBot || exit -------------------------------------------------------------------------------- /Docker/root/payloads/002-install-XYBot-dep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd /home/app/XYBot || exit 4 | pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple -------------------------------------------------------------------------------- /Docker/root/wx-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | WC_AUTO_RESTART=${WC_AUTO_RESTART:-no} 4 | WC_LOG_FILE=${WC_LOG_FILE:-/dev/null} 5 | function wechat() { 6 | while : 7 | do 8 | wechat-start >${WC_LOG_FILE} 2>&1 9 | case ${WC_AUTO_RESTART} in 10 | false|no|n|0) 11 | exit 0 12 | ;; 13 | esac 14 | done 15 | } 16 | 17 | 18 | /entrypoint.sh & 19 | sleep 5 20 | wechat & 21 | xybot-start & 22 | wait -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 公告 2 | 3 | # XYBotV1已停止更新,请前往XYBotV2 https://github.com/HenryXiaoYang/XYBotV2 4 | 5 | # XYBot 微信机器人 6 | 7 |

8 | XYBot微信机器人logo 9 |

10 | 11 | XYBot是一个可运行于Linux和Windows的基于Hook的微信机器人。😊 具有高度可自定义性,支持自我编写插件。🚀 12 | 13 | XYBot提供了多种功能,包括获取天气🌤️、获取新闻📰、Hypixel玩家查询🎮、战争雷霆玩家查询🎮、随机图片📷、随机链接🔗、五子棋♟️、签到✅、查询积分📊、积分榜🏆、积分转送💰、积分抽奖🎁、积分红包🧧等。🎉 14 | 15 | XYBot还提供了AI相关的功能,包括ChatGPT🗣️,Dalle🎨。🤖 16 | 17 | XYBot拥有独立的经济系统,其中基础货币称为”积分“。💰 18 | 19 | XYBot还提供了管理员功能,包括修改积分💰、修改白名单📝、重置签到状态🔄、获取机器人通讯录📚、热加载/卸载/重载插件🔄等。🔒 20 | 21 | XYBot详细的部署教程可以在项目的Wiki中找到。📚 同时,XYBot还支持自我编写插件,用户可以根据自己的需求和创造力编写自定义插件,进一步扩展机器人的功能。💡 22 | 23 | ✅高度可自定义! 24 | ✅支持自我编写插件! 25 | 26 |

27 | GPLv3 License 28 | Version 29 | Blog 30 |

31 | 32 | ## 公告 33 | 34 | 由于需要频繁的更新维护,XYBot版本号格式将会发生变化,v0.0.7后面的版本号将会按照以下格式进行更新: 35 | 36 | v大版本(hook/微信版本变动时更改).功能版本.Bug修复版本 37 | 38 | 例如: 39 | 40 | - v1.0.1是v1.0.0的Bug修复版本 41 | - v1.1.0是v1.0.0的功能版本 42 | - v1.1.1是v1.1.0的Bug修复版本 43 | 44 | ## 功能列表 45 | 46 | 用户功能: 47 | 48 | - 获取天气🌤️ 49 | - 获取新闻📰 50 | - ChatGPT🗣️ 51 | - Dalle🎨 52 | - Hypixel玩家查询🎮 53 | - 战争雷霆玩家信息查询💣 54 | - 随机图图📷 55 | - 随机链接🔗 56 | - 五子棋♟️ 57 | - 签到✅ 58 | - 查询积分📊 59 | - 积分榜🏆 60 | - 积分转送💰 61 | - 积分抽奖🎁 62 | - 积分红包🧧 63 | 64 | 管理员功能: 65 | 66 | - 修改积分💰 67 | - 修改白名单📝 68 | - 重置签到状态🔄 69 | - 获取机器人通讯录📚 70 | - 热加载/卸载/重载插件🔄 71 | - 查看已加载插件ℹ️ 72 | 73 | ## XYBot 文档 📄 74 | 75 | 文档中有完整的功能介绍,部署教程,配置教程,插件编写教程。 76 | 77 | **[🔗XYBot 文档](https://henryxiaoyang.github.io/XYBot)** 78 | 79 | ## 功能演示 80 | 81 | 菜单 82 | ![Menu Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/menu.png?raw=true) 83 | 84 | 随机图片 85 | ![Random Picture Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/random_picture.png?raw=true) 86 | 87 | ChatGPT 88 | ![ChatGPT Example 1](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/gpt3.png?raw=true) 89 | ![ChatGPT Example 2](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/gpt4.png?raw=true) 90 | 91 | 私聊ChatGPT 92 | ![Private ChatGPT Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/private_gpt.png?raw=true) 93 | 94 | 天气查询 95 | ![Weather Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/weather.png?raw=true) 96 | 97 | 五子棋 98 | ![Gomoku Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/gomoku.png?raw=true) 99 | 100 | ## 快速开始🚀 101 | 102 | ### Linux/Docker 103 | ```shell 104 | docker pull henryxiaoyang/xybot:latest 105 | 106 | docker run -d --name XYBot \ 107 | -e WC_AUTO_RESTART=yes \ 108 | -p 4000:8080 \ 109 | --add-host dldir1.qq.com:127.0.0.1 \ 110 | -v XYBot:/home/app/XYBot/ \ 111 | -v XYBot-wechatfiles:/home/app/WeChat\ Files/ \ 112 | --tty \ 113 | henryxiaoyang/xybot:latest 114 | ``` 115 | 116 | ### Windows 117 | 118 | 需要 Git 与 [Python3](https://www.python.org/downloads/release/python-3127/) 与 [微信3.9.10.27](https://github.com/lich0821/WeChatFerry/releases/download/v39.2.4/WeChatSetup-3.9.10.27.exe) 119 | 120 | ```shell 121 | git clone https://github.com/HenryXiaoYang/XYBot.git 122 | cd XYBot 123 | pip install -r requirements.txt 124 | 125 | # 请手动启动微信 126 | 127 | # 启动微信后执行 128 | python start.py 129 | ``` 130 | 131 | ## 自我编写插件🧑‍💻 132 | 133 | 请参考模板插件: 134 | 135 | **[🔗模板插件仓库️](https://github.com/HenryXiaoYang/XYBot-Plugin-Framework)** 136 | 137 | ## XYBot交流群 138 | 139 |

140 | XYBot二维码 141 |

142 | 143 | [**🔗图片会被缓存,点我查看最新二维码**](https://file.yangres.com/xybot-wechatgroup.jpeg) 144 | 145 | ## 捐赠 146 | 147 |

爱发电二维码

148 |

你的赞助是我创作的动力!🙏

149 | 150 | ## FAQ❓❓❓ 151 | 152 | #### ARM架构能不能运行?🤔️ 153 | 154 | 不行 155 | 156 | #### 用的什么微信版本?🤔️ 157 | 158 | 3.9.10.27😄 159 | 160 | #### 最长能运行多久?🤔️ 161 | 162 | XYBot内置了防微信自动退出登录功能,可以保持长时间运行。 163 | 164 | ## 特别感谢 165 | 166 | https://github.com/ChisBread 感谢提供了Docker容器相关的信息! 167 | 168 | https://github.com/lich0821 感谢这个项目的作者写的wcferry! 169 | 170 | ## ⭐️Star History⭐️ 171 | 172 |

173 | 174 | 180 | 186 | XYBot Star History 191 | 192 |

193 | 194 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # XYBot 微信机器人 2 | 3 |

4 | XYBot微信机器人logo 5 |

6 | 7 | XYBot是一个可运行于Linux和Windows的基于Hook的微信机器人。😊 具有高度可自定义性,支持自我编写插件。🚀 8 | 9 | XYBot提供了多种功能,包括获取天气🌤️、获取新闻📰、Hypixel玩家查询🎮、战争雷霆玩家查询🎮、随机图片📷、随机链接🔗、五子棋♟️、签到✅、查询积分📊、积分榜🏆、积分转送💰、积分抽奖🎁、积分红包🧧等。🎉 10 | 11 | XYBot还提供了AI相关的功能,包括ChatGPT🗣️,Dalle🎨。🤖 12 | 13 | XYBot拥有独立的经济系统,其中基础货币称为”积分“。💰 14 | 15 | XYBot还提供了管理员功能,包括修改积分💰、修改白名单📝、重置签到状态🔄、获取机器人通讯录📚、热加载/卸载/重载插件🔄等。🔒 16 | 17 | XYBot详细的部署教程可以在项目的Wiki中找到。📚 同时,XYBot还支持自我编写插件,用户可以根据自己的需求和创造力编写自定义插件,进一步扩展机器人的功能。💡 18 | 19 | ✅高度可自定义! 20 | ✅支持自我编写插件! 21 | 22 |

23 | GPLv3 License 24 | Version 25 | Blog 26 |

27 | 28 | ## 公告 29 | 30 | 由于需要频繁的更新维护,XYBot版本号格式将会发生变化,v0.0.7后面的版本号将会按照以下格式进行更新: 31 | 32 | v大版本(hook/微信版本变动时更改).功能版本.Bug修复版本 33 | 34 | 例如: 35 | 36 | - v1.0.1是v1.0.0的Bug修复版本 37 | - v1.1.0是v1.0.0的功能版本 38 | - v1.1.1是v1.1.0的Bug修复版本 39 | 40 | ## 功能列表 41 | 42 | 用户功能: 43 | 44 | - 获取天气🌤️ 45 | - 获取新闻📰 46 | - ChatGPT🗣️ 47 | - Dalle🎨 48 | - Hypixel玩家查询🎮 49 | - 战争雷霆玩家信息查询💣 50 | - 随机图图📷 51 | - 随机链接🔗 52 | - 五子棋♟️ 53 | - 签到✅ 54 | - 查询积分📊 55 | - 积分榜🏆 56 | - 积分转送💰 57 | - 积分抽奖🎁 58 | - 积分红包🧧 59 | 60 | 管理员功能: 61 | 62 | - 修改积分💰 63 | - 修改白名单📝 64 | - 重置签到状态🔄 65 | - 获取机器人通讯录📚 66 | - 热加载/卸载/重载插件🔄 67 | - 查看已加载插件ℹ️ 68 | 69 | ## XYBot 文档 📄 70 | 71 | 文档中有完整的功能介绍,部署教程,配置教程,插件编写教程。 72 | 73 | **[🔗XYBot 文档](https://henryxiaoyang.github.io/XYBot)** 74 | 75 | ## 功能演示 76 | 77 | 菜单 78 | ![Menu Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/menu.png?raw=true) 79 | 80 | 随机图片 81 | ![Random Picture Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/random_picture.png?raw=true) 82 | 83 | ChatGPT 84 | ![ChatGPT Example 1](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/gpt3.png?raw=true) 85 | ![ChatGPT Example 2](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/gpt4.png?raw=true) 86 | 87 | 私聊ChatGPT 88 | ![Private ChatGPT Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/private_gpt.png?raw=true) 89 | 90 | 天气查询 91 | ![Weather Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/weather.png?raw=true) 92 | 93 | 五子棋 94 | ![Gomoku Example](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/README/gomoku.png?raw=true) 95 | 96 | ## 快速开始🚀 97 | 98 | ### Linux/Docker 99 | ```shell 100 | docker pull henryxiaoyang/xybot:v2.0.0 101 | 102 | docker run -d --name XYBot \ 103 | -e WC_AUTO_RESTART=yes \ 104 | -p 4000:8080 \ 105 | --add-host dldir1.qq.com:127.0.0.1 \ 106 | -v XYBot:/home/app/XYBot/ \ 107 | -v XYBot-wechatfiles:/home/app/WeChat\ Files/ \ 108 | --tty \ 109 | henryxiaoyang/xybot:v2.0.0 110 | ``` 111 | 112 | ### Windows 113 | 114 | 需要 Git 与 [Python3](https://www.python.org/downloads/release/python-3127/) 与 [微信3.9.10.27](https://github.com/lich0821/WeChatFerry/releases/download/v39.2.4/WeChatSetup-3.9.10.27.exe) 115 | 116 | ```shell 117 | git clone https://github.com/HenryXiaoYang/XYBot.git 118 | cd XYBot 119 | pip install -r requirements.txt 120 | 121 | # 请手动启动微信 122 | 123 | # 启动微信后执行 124 | python start.py 125 | ``` 126 | 127 | ## 自我编写插件🧑‍💻 128 | 129 | 请参考模板插件: 130 | 131 | **[🔗模板插件仓库️](https://github.com/HenryXiaoYang/XYBot-Plugin-Framework)** 132 | 133 | ## XYBot交流群 134 | 135 |

136 | XYBot二维码 137 |

138 | 139 | [**🔗图片会被缓存,点我查看最新二维码**](https://file.yangres.com/xybot-wechatgroup.jpeg) 140 | 141 | ## 捐赠 142 | 143 |

爱发电二维码

144 |

你的赞助是我创作的动力!🙏

145 | 146 | ## FAQ❓❓❓ 147 | 148 | #### ARM架构能不能运行?🤔️ 149 | 150 | 不行 151 | 152 | #### 用的什么微信版本?🤔️ 153 | 154 | 3.9.11.25😄 155 | 156 | #### 最长能运行多久?🤔️ 157 | 158 | XYBot内置了防微信自动退出登录功能,可以保持长时间运行。 159 | 160 | ## 特别感谢 161 | 162 | https://github.com/ChisBread 感谢提供了Docker容器相关的信息! 163 | 164 | https://github.com/lich0821 感谢这个项目的作者写的wcferry! 165 | 166 | ## ⭐️Star History⭐️ 167 | 168 |

169 | 170 | 176 | 182 | XYBot Star History 187 | 188 |

189 | 190 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [XYBot介绍](README.md "XYBot 微信机器人") 2 | - [XYBot功能介绍](zh-cn/XYBot功能介绍.md "XYBot 功能介绍") 3 | - [XYBot Linux部署](zh-cn/XYBotLinux部署.md "XYBot Linux部署") 4 | - [XYBot Windows部署](zh-cn/XYBotWindows部署.md "XYBot Windows部署") 5 | - [XYBot的设置](zh-cn/XYBot的设置.md "XYBot的设置") -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XYBot文档 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 |
🏃加载中...🏃
22 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /docs/xybot_logo_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/docs/xybot_logo_32x32.png -------------------------------------------------------------------------------- /docs/zh-cn/XYBotLinux部署.md: -------------------------------------------------------------------------------- 1 | # XYBot Linux部署 2 | 3 | 这一页写了在Linux上部署XYBot的方法。 4 | 5 | 本篇部署教程适用于`XYBot v2.0.0`。 6 | 7 | ## 前言 8 | 9 | 在Linux上部署`XYBot`需要用到`Docker`,`Docker`容器中运用了`wine`,它对环境要求**极高**。 10 | 11 | [已知可以部署的发行版:](https://github.com/ChisBread/wechat-service/issues/1#issuecomment-1252083579) 12 | 13 | - `Ubuntu` 14 | - `Arch` 15 | - `Debian` 16 | - `DSM6.2.3` 17 | - `DSM7.0` 18 | 19 | 不可部署的发行版: 20 | 21 | - `CentOS` 22 | 23 | 欢迎各位开`issue`或者`pull request`来反馈! 24 | 25 | [CentOS部署失败](https://github.com/ChisBread/wechat-service/issues/1) 26 | 27 | 由于运行PC版微信将消耗很多资源,请确认服务器配置。 28 | 29 | 服务器配置要求: 30 | 31 | - 2核4G以上 32 | 33 | ## 部署 34 | 35 | ### 1. 安装Docker 36 | 37 | 装好了可跳过 38 | 39 | 官方教程链接🔗: 40 | 41 | https://docs.docker.com/get-docker/ 42 | 43 | ### 2. 安装Docker Compose 44 | 45 | 一样,已装好可跳过 46 | 47 | https://docs.docker.com/compose/install/ 48 | 49 | ### 3. 拉取Docker镜像 50 | 51 | 这一步以及后面遇到权限问题请在前面加个`sudo`。 52 | 53 | ```bash 54 | docker pull henryxiaoyang/xybot:latest 55 | ``` 56 | 57 | ### 4. 启动容器 58 | 59 | 指令: 60 | ```bash 61 | docker run -d --name XYBot \ 62 | -e WC_AUTO_RESTART=yes \ 63 | -p 4000:8080 \ 64 | --add-host dldir1.qq.com:127.0.0.1 \ 65 | -v XYBot:/home/app/XYBot/ \ 66 | -v XYBot-wechatfiles:/home/app/WeChat\ Files/ \ 67 | --tty \ 68 | henryxiaoyang/xybot:latest 69 | ``` 70 | 71 | Docker-compose: 72 | 73 | `XYBot/Docker/docker-compose.yaml` 74 | 75 | ```yaml 76 | version: "3.3" 77 | 78 | services: 79 | xybot: 80 | image: "henryxiaoyang/xybot:latest" 81 | restart: unless-stopped 82 | container_name: "XYBot" 83 | environment: 84 | WC_AUTO_RESTART: "yes" 85 | ports: 86 | - "4000:8080" 87 | extra_hosts: 88 | - "dldir1.qq.com:127.0.0.1" 89 | volumes: 90 | - "XYBot:/home/app/XYBot/" 91 | - "XYBot-wechatfiles:/home/app/WeChat Files/" 92 | tty: true 93 | 94 | volumes: 95 | XYBot: 96 | XYBot-wechatfiles: 97 | ``` 98 | 99 | ### 5. 容器日志提示你登陆微信后登陆微信 100 | 101 | 在浏览器中打开`http://<你的ip地址>:4000/vnc.html`访问VNC。 102 | 103 | ![VNC WeChat Login](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/wiki/windows_deployment/vnc_wechat_login.png?raw=true) 104 | 105 | 扫描微信二维码并登录,登陆后XYBot将自动启动。 106 | 107 | !>如果遇到微信崩溃,可尝试重启容器重新按步骤登陆。 108 | 109 | ### 6. 配置XYBot设置 110 | 111 | 如果使用的步骤4的启动指令,XYBot的文件已被持久化到`/var/lib/docker/volumes/XYBot`,也就是`XYBot`卷。 112 | 113 | ```bash 114 | cd /var/lib/docker/volumes/XYBot/_data 115 | ``` 116 | 117 | 在这个目录下可以看到`main_config.yml`,修改这个文件即可。 118 | 119 | ### 7. 重启容器 120 | 121 | ```bash 122 | docker restart XYBot 123 | ``` 124 | 125 | 修改主设置后需要重启容器。重启后需要访问VNC重新扫码并登陆微信! 126 | 127 | ### 8. 测试是否部署成功 128 | 129 | 在微信中向XYBot私聊`菜单`,如果返回菜单则部署成功。 130 | 131 | 132 | 133 | #### **HenryXiaoYang** 134 | 135 | 菜单 136 | 137 | #### **XYBot** 138 | 139 | -----XYBot菜单------ 140 | 141 | 实用功能⚙️ 142 | 143 | 1.1 获取天气 144 | 145 | 1.2 获取新闻 146 | 147 | 1.3 ChatGPT 148 | 149 | 1.4 Hypixel玩家查询 150 | 151 | 152 | 153 | 娱乐功能🔥 154 | 155 | 2.1 随机图图 156 | 157 | 2.2 随机链接 158 | 159 | 2.3 随机群成员 160 | 161 | 2.4 五子棋 162 | 163 | 164 | 165 | 积分功能💰 166 | 167 | 3.1 签到 168 | 169 | 3.2 查询积分 170 | 171 | 3.3 积分榜 172 | 173 | 3.4 积分转送 174 | 175 | 3.5 积分抽奖 176 | 177 | 3.6 积分红包 178 | 179 | 180 | 181 | 🔧管理员功能 182 | 183 | 4.1 管理员菜单 184 | 185 | 186 | 187 | 获取菜单指令格式: 菜单 编号 188 | 189 | 例如:菜单 1.1 190 | 191 | 192 | 可以开始用XYBot了! 193 | 194 | 如果失败,可以看看容器日志并发`issue`询问。 195 | 196 | ```bash 197 | docker logs xybot -f --tail 100 198 | ``` 199 | 200 | ### 9. 设置VNC密码 201 | 202 | VNC默认是没有密码的,强烈推荐设置密码。 203 | 204 | #### 1. 进入容器bash 205 | 206 | ```bash 207 | docker exec -it xybot /bin/bash 208 | ``` 209 | 210 | #### 2. 设置密码 211 | 212 | 请设置一个强密码避免暴力破解 213 | 214 | ```bash 215 | # 跟提示设置密码 216 | x11vnc --storepasswd 217 | ``` 218 | 219 | #### 3. 编辑文件 220 | 221 | 将第二行改成: 222 | 223 | ```command=x11vnc -forever -shared -rfbauth /home/app/.vnc/passwd``` 224 | 225 | ```bash 226 | # 修改这个文件 227 | vi /etc/supervisord.d/x11vnc.conf 228 | ``` 229 | 230 | 现在第二行应该是: 231 | 232 | ```command=x11vnc -forever -shared -rfbauth /home/app/.vnc/passwd``` 233 | 234 | #### 4. 退出容器bash 235 | 236 | ```bash 237 | exit 238 | ``` 239 | 240 | #### 5. 重启容器 241 | 242 | ```bash 243 | docker restart xybot 244 | ``` 245 | 246 | 现在用网页连接vnc会请求密码 247 | 248 | #### 6. 登陆VNC后重新扫描二维码登陆微信 249 | 250 | 登陆后,XYBot会自动启动 251 | 252 | 253 | -------------------------------------------------------------------------------- /docs/zh-cn/XYBotWindows部署.md: -------------------------------------------------------------------------------- 1 | # XYBot Windows部署 2 | 3 | 这一页写了在Windows上部署XYBot的方法。 4 | 5 | 本篇部署教程适用于`XYBot v2.0.0`。 6 | 7 | ## 前言 8 | 9 | 相比于在Linux上部署`XYBot`,在Windows上部署`XYBot`简单很多。 10 | 11 | 配置要求: 12 | 13 | - 64位 14 | - 能运行PC版微信 15 | 16 | ## 部署 17 | 18 | ### 1. 安装Python环境 19 | 20 | 请安装`Python3.12`:[🔗链接](https://www.python.org/downloads/release/python-3127/) 21 | 22 | 装好了可跳过 23 | 24 | 如果不知道如何安装请查阅:[Python官方文档](https://docs.python.org/3.9/using/windows.html) 25 | 26 | 看不懂英文的话网上也有很多中文教程 27 | 28 | !> 请注意安装`Python`时将`Add Python 3.12 to PATH`环境变量选项勾选上。 29 | 30 | ### 2. 安装Git 31 | 32 | 装好了可跳过 33 | 34 | 官网下载地址:[🔗链接](https://git-scm.com/download/win) 35 | 36 | 看不懂英文的话网上也有很多中文教程 37 | 38 | ### 3. 下载并安装PC版微信v3.9.10.27 39 | 40 | 下载地址:[🔗链接](https://github.com/tom-snow/wechat-windows-versions/releases/tag/v3.9.10.27) 41 | 42 | 正常安装微信即可。 43 | 44 | ### 4. 从Github克隆XYBot项目 45 | 46 | `git clone`将`XYBot`从Github克隆下来 47 | 48 | ```commandline 49 | git clone https://github.com/HenryXiaoYang/XYBot.git 50 | ``` 51 | 52 | ### 5. 下载XYBot所需要的依赖 53 | 54 | 切换到`XYBot`的目录 55 | 56 | ```commandline 57 | cd XYBot 58 | ``` 59 | 60 | 然后用`pip`安装依赖 61 | 62 | ```commandline 63 | pip install -r requirements.txt 64 | ``` 65 | 66 | 在国内太慢的话看眼选择用镜像源。 67 | 68 | ```commandline 69 | pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 70 | ``` 71 | ### 6. 启动微信 72 | 73 | 请手动启动微信。 74 | 75 | ### 7. 运行XYBot 76 | 77 | 在命令行运行`XYBot`。 78 | 79 | ```commandline 80 | python start.py 81 | ``` 82 | 83 | ### 8. 登陆微信 84 | 85 | 当终端里有提示让你登陆微信时,请在微信中扫描二维码登陆。 86 | 87 | ### 9. 测试是否部署成功 88 | 89 | 可以开始用XYBot了! 90 | 91 | 如果失败,可以看看命令行日志。解决不了的话可以开`issue`询问。 92 | 93 | -------------------------------------------------------------------------------- /docs/zh-cn/XYBot插件编写.md: -------------------------------------------------------------------------------- 1 | # 本文档已过期 2 | 3 | # XYBot插件编写 4 | 5 | 这一页讲述了如何编写一个XYBot插件 6 | 7 | 本篇适用于`XYBot v0.0.6`。 8 | 9 | ## 插件模板 10 | 11 | [🔗插件模版仓库](https://github.com/HenryXiaoYang/XYBot-Plugin-Framework) 12 | 13 | ## 编写你的第一个XYBot插件 14 | 15 | 一个XYBot插件由2个文件组成: 16 | 17 | - 一个`.py`结尾的Python脚本文件 18 | - 一个`.yml`结尾的配置文件 19 | 20 | 我们将编写一个名为`hello_world`的插件。当用户输入`/hello`,XYBot会回复`world!`。 21 | 22 | ### Python脚本文件 23 | 24 | 这是从`XYBot-Plugin-Framework`仓库中的`plugins/my_plugin.py`文件中复制的插件脚本模板: 25 | 26 | ```python 27 | # my_plugin.py 28 | import pywxdll 29 | import yaml 30 | from loguru import logger 31 | 32 | from plugin_interface import PluginInterface 33 | 34 | 35 | # 这里的类名得与插件设置文件中 plugin_name 一样。建议也与文件名一样 36 | class my_plugin(PluginInterface): 37 | def __init__(self): 38 | config_path = 'plugins/my_plugin.yml' 39 | with open(config_path, 'r', encoding='utf-8') as f: # 读取插件设置 40 | config = yaml.safe_load(f.read()) 41 | 42 | # 这里从.yml文件中读取设置 43 | self.plugin_setting = config['plugin_setting'] 44 | 45 | # 这里从主设置中读取微信机器人api的ip地址、端口,并启动 46 | main_config_path = 'main_config.yml' # 主设置文件路径 47 | with open(main_config_path, 'r', encoding='utf-8') as f: # 读取设置 48 | main_config = yaml.safe_load(f.read()) # 读取设置 49 | 50 | self.ip = main_config['ip'] # ip 51 | self.port = main_config['port'] # 端口 52 | self.bot = pywxdll.Pywxdll(self.ip, self.port) # 机器人api 53 | 54 | async def run(self, recv): # 当用户指令与插件设置中关键词相同,执行对应插件的run函数 55 | out_message = 'hello,world! \n plugin_setting:{plugin_setting}'.format( 56 | plugin_setting=self.plugin_setting) # 组建消息 57 | logger.info('[发送信息]{out_message}| [发送到] {wxid}'.format(out_message=out_message, wxid=recv['wxid'])) 58 | self.bot.send_txt_msg(recv['wxid'], 59 | out_message) # 发送消息,更多微信机器人api函数请看 https://github.com/HenryXiaoYang/pywxdll 中的文档 60 | ``` 61 | 62 | 由于XYBot加载插件时,是用设置中的类名实例化插件的,所以我们将这个文件保存为`hello_world.py` 63 | ,并在这里第9行的类名需要改成`hello_world`。 64 | 65 | 对于这个简单的插件,我们不需要从设置中读取任何设置,所以我们可以删除第11-16行用于读取插件设置的代码。我们还是得保留第18-25行的代码,因为我们需要读取主设置中机器人ip与端口,并实例化`pywxdll` 66 | 提供的类。 67 | 68 | 我们先删除第28-32行的代码,后面重新编写。 69 | 70 | 当这个`hello_world`插件的`run`函数被调用时,我们需要向用户发送`world!`。我们可以使用`self.bot.send_txt_msg` 71 | 函数来发送消息。这个函数需要两个参数:`wxid`与`message`。`wxid`是用户的`wxid`,`message`是要发送的消息。 72 | 73 | !> `wxid`不是微信号!`wxid`是一个在微信内部使用的ID,用于识别用户。`wxid`一般以 `wxid_` 开头,例如 `wxid_0123456789abc` 74 | ,但是也有例外情况:一些老的微信账号的`wxid`不一定是以`wxid_`开头,可能是自定义的。 75 | 76 | 我们已经知道要发什么了(`world!`),但是我们不知道要发给谁。 77 | 78 | 从`run`函数的`recv`参数中,我们可以获取到用户的`wxid`。`recv` 79 | 是一个字典,包含了用户的消息的所有信息。我们可以通过`recv['wxid']`来获取调用指令的用户的`wxid`。 80 | 81 | 所以我们可以将`run`函数改为: 82 | 83 | ```python 84 | async def run(self, recv): # 当用户指令与插件设置中关键词相同,执行对应插件的run函数 85 | wxid = recv['wxid'] 86 | ``` 87 | 88 | 好的,我们现在知道要发给谁了。现在就可以发送消息了! 89 | 90 | ```python 91 | async def run(self, recv): # 当用户指令与插件设置中关键词相同,执行对应插件的run函数 92 | wxid = recv['wxid'] 93 | self.bot.send_txt_msg(wxid, 'world!') # 发送消息 94 | ``` 95 | 96 | 最后的代码是: 97 | 98 | ```python 99 | # hello_world.py 100 | import pywxdll 101 | import yaml 102 | from loguru import logger 103 | 104 | from plugin_interface import PluginInterface 105 | 106 | 107 | # 这里的类名得与插件设置文件中 plugin_name 一样。建议也与文件名一样 108 | class hello_world(PluginInterface): 109 | def __init__(self): 110 | # 这里从主设置中读取微信机器人api的ip地址、端口,并启动 111 | main_config_path = 'main_config.yml' # 主设置文件路径 112 | with open(main_config_path, 'r', encoding='utf-8') as f: # 读取设置 113 | main_config = yaml.safe_load(f.read()) # 读取设置 114 | 115 | self.ip = main_config['ip'] # ip 116 | self.port = main_config['port'] # 端口 117 | self.bot = pywxdll.Pywxdll(self.ip, self.port) # 机器人api 118 | 119 | 120 | async def run(self, recv): # 当用户指令与插件设置中关键词相同,执行对应插件的run函数 121 | wxid = recv['wxid'] 122 | self.bot.send_txt_msg(wxid, 'world!') # 发送消息 123 | ``` 124 | 125 | 请注意,一个插件由两个文件组成,我们还需要一个配置文件。 126 | 127 | ### 配置文件 128 | 129 | 这是从`XYBot-Plugin-Framework`仓库中的`plugins/my_plugin.yml`文件中复制的一个插件设置模板: 130 | 131 | ```yaml 132 | # my_plugin.yml 133 | keywords: [ "我的插件","myplugin" ] # 指令关键词 /我的插件 和 /myplugin 都会运行插件 my_plugin 134 | plugin_name: "my_plugin" # 插件名 135 | 136 | plugin_setting: "This is my setting!" # 插件设置,在my_plugin.py被读取 137 | ``` 138 | 139 | 养成好习惯,为了后面维护方便,我们将这个文件保存为`hello_world.yml`。 140 | 141 | 首先我们把第2行的`my_plugin`改成`hello_world`,因为我们的插件名是`hello_world`,* 142 | *XYBot加载插件时是从这里获得到类名的,所以设置中的`plugin_name`必须与Python脚本文件中的类名保持一致!** 143 | 144 | ?> 这样写加载插件的原因是考虑到一个脚本文件可以有多个设置文件 145 | 146 | 对于这个插件,我们不需要`plugin_setting`,所以我们可以删除第4行。 147 | 148 | 将第一行的`keywords`改为`[ "hello" ]`。这样用户在微信输入`/hello`时,XYBot就会运行Python脚本中的`run`函数。 149 | 150 | 最后的配置文件是: 151 | 152 | ```yaml 153 | # hello_world.yml 154 | keywords: [ "hello" ] 155 | plugin_name: "hello_world" # 插件名 156 | ``` 157 | 158 | ### 将插件添加到XYBot 159 | 160 | 将`hello_world.py`与`hello_world.yml`文件放到`plugins`目录下。 161 | 162 | ### 热加载插件 163 | 164 | !> 这里微信账号需要是XYBot管理员,没有的话只能重启XYBot了 165 | 166 | 在微信向XYBot发送`/管理插件 加载 hello_world`,XYBot就会加载`hello_world`插件。 167 | 168 | 169 | 170 | #### **HenryXiaoYang** 171 | 172 | /管理插件 加载 hello_world 173 | 174 | #### **XYBot** 175 | 176 | -----XYBot----- 177 | 加载插件hello_world成功!✅ 178 | 179 | 180 | ### 测试插件 181 | 182 | 现在,当你在微信中输入`/hello`,XYBot会回复`world!`。 183 | 184 | 185 | 186 | #### **HenryXiaoYang** 187 | 188 | /hello 189 | 190 | #### **XYBot** 191 | 192 | world! 193 | 194 | 195 | ## 进阶 196 | 197 | 这只是一个简单的插件,你可以在这个基础上添加更多功能。 198 | 199 | 更多微信机器人api函数请看[🔗文档](https://henryxiaoyang.github.io/pywxdll)。 200 | 201 | 更多机器人插件例子可在[🔗这里](https://github.com/HenryXiaoYang/XYBot/tree/main/plugins)看到。 202 | -------------------------------------------------------------------------------- /docs/zh-cn/XYBot的设置.md: -------------------------------------------------------------------------------- 1 | # XYBot的设置 2 | 3 | 这一页写了如何设置XYBot。 4 | 5 | 本篇适用于`XYBot v2.2.0`。 6 | 7 | ## 配置主设置 8 | 9 | 这是`XYBot v2.2.0`的主设置: 10 | 11 | ```yaml 12 | #Version v2.2.0 13 | bot_version: "v2.2.0" 14 | 15 | # XYBot主设置 16 | 17 | #如果不知道自己在干什么请别动这两行 18 | ip: 127.0.0.1 19 | port: 5555 20 | 21 | admins: [ ] 22 | 23 | command_prefix: "" #如果需要前缀,则必须为一个字符。如果不需要前缀可设置为空,即 ""。 24 | 25 | excluded_plugins: [ "" ] 26 | 27 | timezone: "Asia/Shanghai" 28 | 29 | # ------------------------------------------------------------------------------ # 30 | 31 | # 白名单/黑名单设置 32 | mode: "none" # 可以是 黑名单blacklist 白名单whitelist 无none 33 | 34 | # 请填入群号或者wxid 35 | blacklist: [ ] 36 | whitelist: [ ] 37 | 38 | # ------------------------------------------------------------------------------ # 39 | # 全局chatgpt设置,插件使用到,私聊gpt也使用到 40 | 41 | #ChatGPT的API网址 42 | openai_api_base: "" 43 | #ChatGPT API的Key 44 | openai_api_key: "" 45 | ``` 46 | 47 | - `bot_version`是机器人版本号 48 | - `ip` `port` `tcp_server_port` 分别是机器人Hook API的IP地址、端口和接收Hook消息的TCP Server的端口。 49 | - `admins`为管理员的`wxid`,管理员可以有多个,每个管理员的`wxid`用逗号分割。 50 | - `command_prefix`为机器人指令前缀。一般是一个字符,如果不需要前缀可设置为空,即 `""`。 51 | - `excluded_plugins`为你不想启用的插件,可填入插件名。 52 | - `timezone`为机器人所使用的时区,可以查看[时区列表](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)。 53 | 54 | - `mode`设置了机器人的白名单或者黑名单。可填入`blacklist`或`whitelist`。 55 | - `blacklist`使用黑名单时,可填入`wxid`或者群ID。 56 | - `whitelist`使用白名单时,可填入`wxid`或者群ID。 57 | 58 | - `openai_api_base` OpenAI的API密钥。 59 | - `openai_api_base` OpenAI的API网址。 60 | 61 | ## 配置插件设置 62 | 63 | 这里将用`gpt`插件为例,`gpt`插件的配置文件如下: 64 | 65 | ```yaml 66 | keywords: [ "gpt","GPT","ChatGPT","chatgpt","CHATGPT"] 67 | plugin_name: "gpt" 68 | 69 | gpt_point_price: 3 70 | 71 | gpt_version: 'gpt-3.5-turbo' 72 | gpt_max_token: 1000 73 | gpt_temperature: 0.5 74 | ``` 75 | 76 | - `keywords`是插件的关键词,当用户发送的指令第一项是任何一个关键词时,机器人会执行插件。 77 | - `plugin_name`**这个千万不要改,程序中是用的这个来实例化插件类的**。 78 | - `gpt_point_price`是ChatGPT插件的对话消耗积分,如果需要变更对话消耗积分,需要在`gpt_point_price`中修改积分数。 79 | - 后面的三个参数是ChatGPT请求模型设置,如果需要变更ChatGPT请求模型设置,需要在`gpt_version`、`gpt_max_token` 80 | 和`gpt_temperature`中修改参数。 81 | 82 | 再用`weather`插件为例,`weather`插件的配置文件如下: 83 | 84 | ```yaml 85 | keywords: [ "天气", "获取天气" ] 86 | plugin_name: "weather" 87 | 88 | weather_api_key: "" 89 | 90 | # 老api改收费了 新的api需要申请,链接:https://dev.qweather.com/ 91 | # 项目订阅选免费订阅即可,把获得到的Key (不是Public ID 而是Private KEY) 填到上面引号中 92 | ``` 93 | 94 | 实时天气这种插件比较特殊,需要申请一个API Key。一般需要API Key的插件都会写注释说明如何获得到API Key。对于`weather` 95 | 插件,需要前往`https://dev.qweather.com/` 申请API Key,然后将获得到的密钥填入`weather_api_key`中。 -------------------------------------------------------------------------------- /docs/zh-cn/_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/logo/xybot_logo_medium.png?raw=true) 2 | 3 | # XYBot v2.0.0 4 | 5 | > 可运行于Linux和Windows的基于Hook的微信机器人🤖️! 6 | 7 | - ✅高度可自定义! 8 | - ✅支持自我编写插件! 9 | - 非常多的功能:天气🌤️、新闻📰、ChatGPT🗣️、AI绘图✍️、Hypixel玩家查询🎮、战争雷霆玩家查询🎮、随机图片📷、随机链接🔗、随机群成员👥、五子棋♟️、签到✅、查询积分📊、积分榜🏆、积分转送💰、积分抽奖🎁、积分红包🧧等 10 | 11 | [GitHub](https://github.com/HenryXiaoYang/XYBot) 12 | [开始使用](README.md) -------------------------------------------------------------------------------- /main_config.yml: -------------------------------------------------------------------------------- 1 | #Version v2.2.1 2 | bot_version: "v2.2.1" 3 | 4 | # XYBot主设置 5 | 6 | #如果不知道自己在干什么请别动这两行 7 | ip: 127.0.0.1 8 | port: 5555 9 | 10 | admins: [ ] 11 | 12 | command_prefix: "" #如果需要前缀,则必须为一个字符。如果不需要前缀可设置为空,即 ""。 13 | 14 | excluded_plugins: [ "" ] 15 | 16 | timezone: "Asia/Shanghai" 17 | 18 | # ------------------------------------------------------------------------------ # 19 | 20 | # 白名单/黑名单设置 21 | mode: "none" # 可以是 黑名单blacklist 白名单whitelist 无none 22 | 23 | # 请填入群号或者wxid 24 | blacklist: [ ] 25 | whitelist: [ ] 26 | 27 | # ------------------------------------------------------------------------------ # 28 | # 全局chatgpt设置,插件使用到,私聊gpt也使用到 29 | 30 | #ChatGPT的API网址 31 | openai_api_base: "" 32 | #ChatGPT API的Key 33 | openai_api_key: "" -------------------------------------------------------------------------------- /plans/antiautolog.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | import schedule 5 | import yaml 6 | from loguru import logger 7 | from wcferry import client 8 | 9 | from utils.plans_interface import PlansInterface 10 | 11 | 12 | class antiautolog(PlansInterface): 13 | def __init__(self): 14 | main_config_path = "main_config.yml" 15 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 16 | main_config = yaml.safe_load(f.read()) 17 | self.timezone = main_config["timezone"] 18 | 19 | async def job(self, bot: client.Wcf): 20 | out_message = f"防微信自动退出登录[{random.randint(1, 9999)}]" # 组建信息 21 | logger.info(f'[发送信息]{out_message}| [发送到] {"filehelper"}') # 直接发到文件传输助手,这样就不用单独键个群辣 22 | bot.send_text("filehelper", out_message) # 发送 23 | 24 | def job_async(self, bot: client.Wcf): 25 | loop = asyncio.get_running_loop() 26 | loop.create_task(self.job(bot)) 27 | 28 | def run(self, bot): 29 | schedule.every(10).minutes.do(self.job_async, bot) # 每10分钟执行一次 30 | -------------------------------------------------------------------------------- /plans/cache_clear.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import schedule 5 | import yaml 6 | from loguru import logger 7 | from wcferry import client 8 | 9 | from utils.plans_interface import PlansInterface 10 | 11 | 12 | class cache_clear(PlansInterface): 13 | def __init__(self): 14 | main_config_path = "main_config.yml" 15 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 16 | main_config = yaml.safe_load(f.read()) 17 | 18 | self.timezone = main_config["timezone"] # 时区 19 | 20 | async def job(self): 21 | path = "resources/cache/" # 图片缓存路径 22 | for filename in os.listdir(path): # 遍历文件夹 23 | file_path = os.path.join(path, filename) # 获取文件路径 24 | # 判断路径是否为文件 25 | if os.path.isfile(file_path): # 如果是文件 26 | # 删除文件 27 | os.remove(file_path) 28 | logger.info("[计划]清除缓存成功") # 记录日志 29 | 30 | def job_async(self): 31 | loop = asyncio.get_running_loop() 32 | loop.create_task(self.job()) 33 | 34 | def run(self, bot: client.Wcf): 35 | schedule.every(6).hours.do(self.job_async) # 每六小时执行一次 36 | -------------------------------------------------------------------------------- /plans/daily_greeting.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import asyncio 6 | import random 7 | from datetime import datetime 8 | 9 | import pytz 10 | import requests 11 | import schedule 12 | import yaml 13 | from loguru import logger 14 | from wcferry import client 15 | 16 | from utils.plans_interface import PlansInterface 17 | 18 | 19 | class daily_greeting(PlansInterface): 20 | def __init__(self): 21 | main_config_path = "main_config.yml" 22 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 23 | main_config = yaml.safe_load(f.read()) 24 | 25 | self.timezone = main_config["timezone"] # 时区 26 | 27 | async def job(self, bot: client.Wcf): 28 | week_names = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] 29 | 30 | now = datetime.now(tz=pytz.timezone(self.timezone)) 31 | 32 | date_str = now.strftime('%Y年%m月%d日') 33 | week_name = week_names[now.weekday()] 34 | daily_sentence = self.get_daily_sentence_formatted() 35 | history_today = self.get_history_today() 36 | 37 | message = f"早上好!☀️今天是{date_str} {week_name}。😆\n\n{daily_sentence}\n\n{history_today}" 38 | 39 | contact_list = bot.get_contacts() 40 | for contact in contact_list: 41 | if str(contact.get("wxid")).endswith("@chatroom"): # 是一个群聊 42 | bot.send_text(message, contact.get("wxid")) 43 | logger.info(f"[发送@信息]{message}| [发送到] {contact.get('wxid')}") 44 | 45 | @staticmethod 46 | def get_daily_sentence_formatted() -> str: 47 | hitokoto_api_url = "https://v1.hitokoto.cn/?encode=json&charset=utf-8" 48 | 49 | hitokoto_api_json = requests.get(hitokoto_api_url).json() 50 | 51 | sentence = hitokoto_api_json.get("hitokoto", "") 52 | from_type = hitokoto_api_json.get("from", "") 53 | from_who = hitokoto_api_json.get("from_who", "") 54 | 55 | from_sentence = f"——{from_type+' '}{from_who}" 56 | 57 | formatted = f"「{sentence}」\n{from_sentence}" 58 | 59 | return formatted 60 | 61 | @staticmethod 62 | def get_history_today() -> str: 63 | url = "https://api.03c3.cn/api/history" 64 | req = requests.get(url) 65 | response_code = req.status_code 66 | response = req.json() 67 | 68 | if response_code != 200 or response.get("code") != 200: 69 | return "" 70 | 71 | data = response.get("data") 72 | data = random.choice(data) 73 | 74 | message = f"🕰️历史上的今天:\n在{data.get('year')}年的今天,{data.get('description')}。" 75 | 76 | return message 77 | 78 | 79 | 80 | 81 | def job_async(self, bot: client.Wcf): 82 | loop = asyncio.get_running_loop() 83 | loop.create_task(self.job(bot)) 84 | 85 | def run(self, bot: client.Wcf): 86 | schedule.every().day.at("07:00", tz=self.timezone).do(self.job_async, bot) 87 | -------------------------------------------------------------------------------- /plans/expired_red_packets_check.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import schedule 4 | import yaml 5 | from wcferry import client 6 | 7 | from utils.plans_interface import PlansInterface 8 | from utils.plugin_manager import plugin_manager 9 | 10 | 11 | class expired_red_packets_check(PlansInterface): 12 | def __init__(self): 13 | config_path = "plugins/command/red_packet.yml" 14 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 15 | config = yaml.safe_load(f.read()) 16 | 17 | self.max_time = config["max_time"] # 红包超时时间 18 | 19 | async def job(self): 20 | if "red_packet" in plugin_manager.plugins.keys(): 21 | plugin_manager.plugins["command"]["red_packet"].expired_red_packets_check() 22 | 23 | def job_async(self): 24 | loop = asyncio.get_running_loop() 25 | loop.create_task(self.job()) 26 | 27 | def run(self, bot: client.Wcf): 28 | schedule.every(self.max_time).minutes.do(self.job_async) # 每60分钟执行一次 29 | -------------------------------------------------------------------------------- /plugins/command/_at_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | 6 | import re 7 | 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.database import BotDatabase 12 | from utils.plugin_interface import PluginInterface 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class at_test(PluginInterface): 17 | def __init__(self): 18 | self.db = BotDatabase() 19 | 20 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 21 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 22 | 23 | out_message = f"@{self.db.get_nickname(recv.sender)} At test!!!" 24 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 25 | bot.send_text(out_message, recv.roomid, recv.sender) 26 | 27 | out_message = str(recv.content) 28 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 29 | bot.send_text(out_message, recv.roomid) 30 | -------------------------------------------------------------------------------- /plugins/command/_blocker.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import asyncio 6 | 7 | from loguru import logger 8 | from wcferry import client 9 | 10 | from utils.plugin_interface import PluginInterface 11 | from wcferry_helper import XYBotWxMsg 12 | 13 | 14 | class blocker(PluginInterface): 15 | def __init__(self): 16 | pass 17 | 18 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 19 | logger.debug("开始执行blocker插件,10s") 20 | await asyncio.sleep(10) 21 | logger.debug("结束执行blocker插件") 22 | -------------------------------------------------------------------------------- /plugins/command/admin_points.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.database import BotDatabase 12 | from utils.plugin_interface import PluginInterface 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class admin_points(PluginInterface): 17 | def __init__(self): 18 | config_path = "plugins/command/admin_points.yml" 19 | with open(config_path, "r", encoding="utf-8") as f: # 读取插件设置 20 | config = yaml.safe_load(f.read()) 21 | 22 | self.command_format_menu = config["command_format_menu"] # 获取指令格式 23 | 24 | main_config_path = "main_config.yml" 25 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 26 | main_config = yaml.safe_load(f.read()) 27 | 28 | self.admin_list = main_config["admins"] # 获取管理员列表 29 | self.db = BotDatabase() # 实例化数据库类 30 | 31 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 32 | recv.content = re.split(" ", recv.content) # 拆分消息 33 | 34 | admin_wxid = recv.sender # 获取发送者wxid 35 | 36 | error = '' 37 | if admin_wxid not in self.admin_list: 38 | error = "-----XYBot-----\n❌你配用这个指令吗?" 39 | elif len(recv.content) < 3: 40 | error = f"-----XYBot-----\n⚠️指令格式错误!\n\n{self.command_format_menu}" 41 | elif recv.content[1] not in ["加", "减"] and not recv.content[1].isnumeric(): 42 | error = f"-----XYBot-----\n⚠️指令格式错误!\n\n{self.command_format_menu}" 43 | 44 | if not error: 45 | 46 | if recv.content[1].isnumeric(): # 直接改变,不加/减 47 | if recv.content[2].startswith('@') and recv.ats: # 判断是@还是wxid 48 | change_wxid = recv.ats[-1] 49 | else: 50 | change_wxid = recv.content[2] 51 | 52 | self.db.set_points(change_wxid, int(recv.content[1])) 53 | 54 | nickname = self.db.get_nickname(change_wxid) # 尝试获取昵称 55 | 56 | out_message = f'-----XYBot-----\n😊成功将 {change_wxid} {nickname if nickname else ""} 的积分设置为 {recv.content[1]}!' 57 | await self.send_friend_or_group(bot, recv, out_message) 58 | 59 | 60 | elif recv.content[1] == "加": # 操作是加分 61 | if recv.content[3].startswith('@') and recv.ats: # 判断是@还是wxid 62 | change_wxid = recv.ats[-1] 63 | else: 64 | change_wxid = recv.content[3] 65 | 66 | self.db.add_points(change_wxid, int(recv.content[2])) # 修改积分 67 | 68 | nickname = self.db.get_nickname(change_wxid) # 尝试获取昵称 69 | new_point = self.db.get_points(change_wxid) # 获取修改后积分 70 | 71 | out_message = f'-----XYBot-----\n😊成功给 {change_wxid} {nickname if nickname else ""} 加了 {recv.content[2]} 点积分,他现在有 {new_point} 点积分!' 72 | await self.send_friend_or_group(bot, recv, out_message) 73 | 74 | elif recv.content[1] == "减": # 操作是减分 75 | if recv.content[3].startswith('@'): # 判断是@还是wxid 76 | change_wxid = recv.ats[-1] 77 | else: 78 | change_wxid = recv.content[3] 79 | 80 | self.db.add_points(change_wxid, int(recv.content[2]) * -1) # 修改积分 81 | 82 | nickname = self.db.get_nickname(change_wxid) # 尝试获取昵称 83 | new_point = self.db.get_points(change_wxid) # 获取修改后积分 84 | 85 | out_message = f'-----XYBot-----\n😊成功给 {change_wxid} {nickname if nickname else ""} 减了 {recv.content[2]} 点积分,他现在有 {new_point} 点积分!' 86 | await self.send_friend_or_group(bot, recv, out_message) 87 | 88 | else: 89 | error = f"-----XYBot-----\n\n⚠️指令格式错误!\n{self.command_format_menu}" 90 | logger.info(f'[发送信息]{error}| [发送到] {recv.roomid}') 91 | bot.send_text(error, recv.roomid) 92 | 93 | 94 | else: # 发送错误信息 95 | out_message = error 96 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 97 | bot.send_text(out_message, recv.roomid) 98 | 99 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 100 | if recv.from_group(): # 判断是群还是私聊 101 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 102 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 103 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 104 | 105 | else: 106 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 107 | bot.send_text(out_message, recv.roomid) # 发送 108 | -------------------------------------------------------------------------------- /plugins/command/admin_points.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "管理积分" ] 2 | plugin_name: "admin_points" 3 | 4 | command_format_menu: "➕加积分:\n管理积分 加 积分数 wxid或者@群成员\n\n➖减积分:\n管理积分 减 积分数 wxid或者@群成员\n\n⚙️直接设置积分:\n管理积分 积分数 wxid或者@群成员" -------------------------------------------------------------------------------- /plugins/command/admin_signin_reset.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.database import BotDatabase 12 | from utils.plugin_interface import PluginInterface 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class admin_signin_reset(PluginInterface): 17 | def __init__(self): 18 | main_config_path = "main_config.yml" 19 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 20 | main_config = yaml.safe_load(f.read()) 21 | 22 | self.admin_list = main_config["admins"] # 获取管理员列表 23 | 24 | self.db = BotDatabase() # 实例化数据库类 25 | 26 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 27 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 28 | 29 | admin_wxid = recv.sender 30 | 31 | if admin_wxid in self.admin_list: # 如果操作人在白名单内 32 | self.db.reset_stat() # 重置数据库签到状态 33 | out_message = "-----XYBot-----\n😊成功重置签到状态!" 34 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 35 | bot.send_text(out_message, recv.roomid) # 发送信息 36 | 37 | else: # 操作人不在白名单内 38 | out_message = "-----XYBot-----\n❌你配用这个指令吗?" 39 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 40 | bot.send_text(out_message, recv.roomid) # 发送信息 41 | -------------------------------------------------------------------------------- /plugins/command/admin_signin_reset.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "重置签到","重置签到冷却","重置签到状态" ] 2 | plugin_name: "admin_signin_reset" -------------------------------------------------------------------------------- /plugins/command/admin_whitelist.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.database import BotDatabase 12 | from utils.plugin_interface import PluginInterface 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class admin_whitelist(PluginInterface): 17 | def __init__(self): 18 | config_path = "plugins/command/admin_whitelist.yml" 19 | with open(config_path, "r", encoding="utf-8") as f: # 读取插件设置 20 | config = yaml.safe_load(f.read()) 21 | 22 | self.command_format_menu = config["command_format_menu"] # 获取命令格式 23 | 24 | main_config_path = "main_config.yml" 25 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 26 | main_config = yaml.safe_load(f.read()) 27 | 28 | self.admin_list = main_config["admins"] # 获取管理员列表 29 | 30 | self.db = BotDatabase() # 实例化数据库类 31 | 32 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 33 | recv.content = re.split(" ", recv.content) # 拆分消息 34 | logger.debug(recv.content) 35 | 36 | admin_wxid = recv.sender # 获取发送者wxid 37 | 38 | error = "" 39 | if admin_wxid not in self.admin_list: # 判断是否为管理员 40 | error = "-----XYBot-----\n❌你配用这个指令吗?" 41 | elif len(recv.content) < 3: # 判断命令格式是否正确 42 | error = f"-----XYBot-----\n命令格式错误❌\n\n{self.command_format_menu}" 43 | 44 | if not error: 45 | if recv.content[2].startswith("@") and recv.ats: 46 | wxid = recv.ats[-1] 47 | else: 48 | wxid = recv.content[2] 49 | 50 | if recv.content[1] == "加入": 51 | self.db.set_whitelist(wxid, 1) 52 | 53 | nickname = self.db.get_nickname(wxid) # 尝试获取昵称 54 | 55 | out_message = f"-----XYBot-----\n成功添加 {wxid} {nickname if nickname else ""} 到白名单!😊" 56 | await self.send_friend_or_group(bot, recv, out_message) 57 | 58 | elif recv.content[1] == "移除": 59 | self.db.set_whitelist(wxid, 0) 60 | 61 | nickname = self.db.get_nickname(wxid) # 尝试获取昵称 62 | 63 | out_message = f"-----XYBot-----\n成功把 {wxid} {nickname if nickname else ""} 移出白名单!😊" 64 | await self.send_friend_or_group(bot, recv, out_message) 65 | 66 | else: 67 | error = f"-----XYBot-----\n未知的操作❌\n\n{self.command_format_menu}" 68 | await self.send_friend_or_group(bot, recv, error) 69 | else: 70 | await self.send_friend_or_group(bot, recv, error) 71 | 72 | 73 | 74 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 75 | if recv.from_group(): # 判断是群还是私聊 76 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 77 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 78 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 79 | 80 | else: 81 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 82 | bot.send_text(out_message, recv.roomid) # 发送 83 | -------------------------------------------------------------------------------- /plugins/command/admin_whitelist.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "管理白名单", "白名单", "whitelist" ] 2 | plugin_name: "admin_whitelist" 3 | 4 | command_format_menu: "➕加入:\n管理白名单 加入 wxid或者@群成员\n\n➖移除:\n管理白名单 移除 wxid或者@群成员" -------------------------------------------------------------------------------- /plugins/command/at_test.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "atme" ] 2 | plugin_name: "at_test" 3 | -------------------------------------------------------------------------------- /plugins/command/blocker.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "block" ] 2 | plugin_name: "blocker" -------------------------------------------------------------------------------- /plugins/command/bot_status.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import base64 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.plugin_interface import PluginInterface 12 | from wcferry_helper import XYBotWxMsg 13 | 14 | 15 | class bot_status(PluginInterface): 16 | def __init__(self): 17 | config_path = "plugins/command/bot_status.yml" 18 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 19 | config = yaml.safe_load(f.read()) 20 | 21 | self.status_message = config["status_message"] # 状态信息 22 | 23 | main_config_path = "main_config.yml" 24 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 25 | main_config = yaml.safe_load(f.read()) 26 | 27 | self.bot_version = main_config["bot_version"] 28 | 29 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 30 | b, a = [ 31 | 82, 32 | 50, 33 | 108, 34 | 48, 35 | 97, 36 | 72, 37 | 86, 38 | 105, 39 | 79, 40 | 105, 41 | 66, 42 | 111, 43 | 100, 44 | 72, 45 | 82, 46 | 119, 47 | 99, 48 | 122, 49 | 111, 50 | 118, 51 | 76, 52 | 50, 53 | 100, 54 | 112, 55 | 100, 56 | 71, 57 | 104, 58 | 49, 59 | 89, 60 | 105, 61 | 53, 62 | 106, 63 | 98, 64 | 50, 65 | 48, 66 | 118, 67 | 83, 68 | 71, 69 | 86, 70 | 117, 71 | 99, 72 | 110, 73 | 108, 74 | 89, 75 | 97, 76 | 87, 77 | 70, 78 | 118, 79 | 87, 80 | 87, 81 | 70, 82 | 117, 83 | 90, 84 | 121, 85 | 57, 86 | 89, 87 | 87, 88 | 85, 89 | 74, 90 | 118, 91 | 100, 92 | 65, 93 | 61, 94 | 61, 95 | ], "" # 嘿嘿 96 | for i in b: 97 | a += chr(i) 98 | out_message = f"-----XYBot-----\n{self.status_message}\nBot version: {self.bot_version}\n{base64.b64decode(a).decode('utf-8')}" 99 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 100 | bot.send_text(out_message, recv.roomid) # 发送 101 | bot.send_pat_msg(recv.roomid, recv.sender) # 发送拍一拍消息 102 | -------------------------------------------------------------------------------- /plugins/command/bot_status.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "机器人状态","bottest" ] 2 | plugin_name: "bot_status" 3 | 4 | status_message: "Bot Running😊" -------------------------------------------------------------------------------- /plugins/command/dalle3.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | # 5 | # This program is licensed under the GNU General Public License v3.0. 6 | 7 | import base64 8 | import os 9 | import re 10 | import time 11 | 12 | import yaml 13 | from loguru import logger 14 | from openai import AsyncOpenAI 15 | from wcferry import client 16 | 17 | from utils.database import BotDatabase 18 | from utils.plugin_interface import PluginInterface 19 | from wcferry_helper import XYBotWxMsg 20 | 21 | 22 | class dalle3(PluginInterface): 23 | def __init__(self): 24 | config_path = "plugins/command/dalle3.yml" 25 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 26 | config = yaml.safe_load(f.read()) 27 | 28 | self.price = config["price"] # 每次使用的积分 29 | 30 | self.model_name = config["model_name"] # dalle3模型 31 | self.image_quality = config["image_quality"] # 生成的图片的质量 32 | self.image_size = config["image_size"] # 生成的图片的大小 33 | self.image_style = config["image_style"] # 生成的图片的风格 34 | 35 | self.command_format_menu = config["command_format_menu"] # 帮助菜单 36 | 37 | main_config_path = "main_config.yml" 38 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 39 | main_config = yaml.safe_load(f.read()) 40 | 41 | self.admins = main_config["admins"] # 管理员列表 42 | 43 | self.openai_api_base = main_config["openai_api_base"] # openai api 链接 44 | self.openai_api_key = main_config["openai_api_key"] # openai api 密钥 45 | 46 | sensitive_words_path = "sensitive_words.yml" # 加载敏感词yml 47 | with open(sensitive_words_path, "r", encoding="utf-8") as f: # 读取设置 48 | sensitive_words_config = yaml.safe_load(f.read()) 49 | self.sensitive_words = sensitive_words_config["sensitive_words"] # 敏感词列表 50 | 51 | self.db = BotDatabase() 52 | 53 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 54 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 55 | 56 | user_wxid = recv.sender # 获取发送者wxid 57 | user_request_prompt = " ".join(recv.content) 58 | 59 | error = "" 60 | if len(recv.content) < 2: # 指令格式正确 61 | error = f"-----XYBot-----\n参数错误!🙅\n\n{self.command_format_menu}" 62 | # 检查积分是否足够,管理员与白名单不需要检查 63 | elif user_wxid not in self.admins and self.db.get_whitelist(user_wxid) == 0 and self.db.get_points( 64 | user_wxid) < self.price: 65 | error = f"-----XYBot-----\n积分不足!😭需要 {self.price} 点积分!" 66 | elif not self.senstitive_word_check(user_request_prompt): # 敏感词检查 67 | error = "-----XYBot-----\n内容包含敏感词!⚠️" 68 | elif not user_request_prompt: 69 | error = f"-----XYBot-----\n请输入描述!🤔\n\n{self.command_format_menu}" 70 | 71 | if error: # 如果没满足生成图片的条件,向用户发送为什么 72 | await self.send_friend_or_group(bot, recv, error) 73 | return 74 | 75 | await self.send_friend_or_group(bot, recv, "-----XYBot-----\n正在生成图片,请稍等...🤔") 76 | 77 | image_path = await self.dalle3(user_request_prompt) 78 | 79 | if isinstance(image_path, Exception): # 如果出现错误,向用户发送错误信息 80 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n出现错误,未扣除积分!⚠️\n{image_path}") 81 | return 82 | 83 | if user_wxid not in self.admins and self.db.get_whitelist(user_wxid) == 0: # 如果用户不是管理员或者白名单,扣积分 84 | self.db.add_points(user_wxid, -self.price) 85 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n🎉图片生成完毕,已扣除 {self.price} 点积分!🙏") 86 | bot.send_pat_msg(recv.roomid, user_wxid) # 拍一拍 87 | 88 | bot.send_image(image_path, recv.roomid) 89 | logger.info(f'[发送图片]{image_path}| [发送到] {recv.roomid}') 90 | 91 | async def dalle3(self, prompt): # 返回生成的图片的绝对路径,报错的话返回错误 92 | client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) 93 | try: 94 | image_generation = await client.images.generate( 95 | prompt=prompt, 96 | model=self.model_name, 97 | n=1, 98 | response_format="b64_json", 99 | quality=self.image_quality, 100 | size=self.image_size) 101 | 102 | image_b64decode = base64.b64decode(image_generation.data[0].b64_json) 103 | save_path = os.path.abspath(f"resources/cache/dalle3_{time.time_ns()}.png") 104 | with open(save_path, "wb") as f: 105 | f.write(image_b64decode) 106 | except Exception as e: 107 | return e 108 | 109 | return save_path 110 | 111 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message: str): 112 | if recv.from_group(): # 判断是群还是私聊 113 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 114 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 115 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 116 | else: 117 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 118 | bot.send_text(out_message, recv.roomid) # 发送信息 119 | 120 | def senstitive_word_check(self, message): # 检查敏感词 121 | for word in self.sensitive_words: 122 | if word in message: 123 | return False 124 | return True 125 | -------------------------------------------------------------------------------- /plugins/command/dalle3.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "dalle","生成图片", "ai图片", "AI图片", "AI绘图", "ai绘图", "aipicture", "AIpicture" ] 2 | plugin_name: "dalle3" 3 | 4 | command_format_menu: "生成AI图片:\nAI图片 关键词" 5 | 6 | # 每次生成消耗的积分 7 | price: 5 8 | 9 | # https://platform.openai.com/docs/api-reference/images/create 10 | model_name: "dall-e-3" # 模型名 11 | image_quality: "standard" # 生成质量,更好的质量可设置为"hd" 12 | image_size: "1024x1024" # 图片大小 13 | image_style: "vivid" # 图片风格 -------------------------------------------------------------------------------- /plugins/command/food_selector.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | from wcferry import client 3 | from utils.plugin_interface import PluginInterface 4 | from wcferry_helper import XYBotWxMsg 5 | import random 6 | import yaml 7 | 8 | class food_selector(PluginInterface): 9 | def __init__(self): 10 | config_path = "plugins/command/food_selector.yml" 11 | with open(config_path, "r", encoding="utf-8") as f: 12 | config = yaml.safe_load(f.read()) 13 | 14 | self.food_options = config["food_options"] 15 | self.command_format_menu = config["command_format_menu"] 16 | 17 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 18 | selected_food = random.choice(self.food_options) 19 | message = ( 20 | f"-----XYBot-----\n" 21 | f"🍽️随机选择的外卖品种是:{selected_food}\n\n" 22 | f"💝温馨提示:\n" 23 | f"🌟 记得吃饭要细嚼慢咽哦\n" 24 | f"🌟 工作再忙也要按时吃饭\n" 25 | f"🌟 注意营养均衡,保重身体\n" 26 | f"祝您用餐愉快!😊" 27 | ) 28 | 29 | await self.send_friend_or_group(bot, recv, message) 30 | logger.info(f"[食物选择] wxid: {recv.sender} | 选择结果: {selected_food}") 31 | 32 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 33 | if recv.from_group(): 34 | out_message = f"@{recv.sender}\n{out_message}" 35 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 36 | bot.send_text(out_message, recv.roomid, recv.sender) 37 | else: 38 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 39 | bot.send_text(out_message, recv.roomid) 40 | -------------------------------------------------------------------------------- /plugins/command/food_selector.yml: -------------------------------------------------------------------------------- 1 | keywords: ["选外卖"] 2 | plugin_name: "food_selector" 3 | 4 | command_format_menu: "🍽️选择外卖品种:\n选外卖\n示例:选外卖" 5 | 6 | food_options: 7 | - "宫保鸡丁饭" 8 | - "麻婆豆腐饭" 9 | - "红烧肉饭" 10 | - "糖醋里脊饭" 11 | - "鱼香肉丝饭" 12 | - "回锅肉饭" 13 | - "黄焖鸡米饭" 14 | - "卤肉饭" 15 | - "叉烧饭" 16 | - "烤肉拌饭" 17 | - "干炒牛河" 18 | - "扬州炒饭" 19 | - "披萨" 20 | - "寿司套餐" 21 | - "汉堡套餐" 22 | - "意大利肉酱面" 23 | - "奶油蘑菇意面" 24 | - "泰式打抛猪肉饭" 25 | - "泰式咖喱鸡饭" 26 | - "印度咖喱鸡饭" 27 | - "印度烤肉饭" 28 | - "韩式石锅拌饭" 29 | - "韩式烤肉饭" 30 | - "日式天妇罗盖饭" 31 | - "日式炸猪排饭" 32 | - "越南牛肉河粉" 33 | - "墨西哥卷饼套餐" 34 | - "烤肉饭" 35 | - "麻辣香锅" 36 | - "水煮鱼饭" 37 | - "酸菜鱼饭" 38 | - "铁板牛肉饭" 39 | - "三杯鸡饭" 40 | - "辣子鸡饭" 41 | - "葱爆羊肉饭" 42 | - "蒜苔炒肉饭" 43 | - "青椒肉丝饭" 44 | - "土豆烧牛肉饭" 45 | - "酱爆茄子饭" 46 | - "尖椒炒牛肉饭" 47 | - "蔬菜色拉" 48 | - "米线" 49 | - "味千拉面" 50 | - "寿喜锅" 51 | - "麻辣烫" 52 | - "拌将" 53 | - "毛血旺" 54 | -------------------------------------------------------------------------------- /plugins/command/get_contact_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import os.path 6 | import re 7 | import time 8 | 9 | import yaml 10 | from loguru import logger 11 | from openpyxl import Workbook 12 | from wcferry import client 13 | 14 | from utils.plugin_interface import PluginInterface 15 | from wcferry_helper import XYBotWxMsg 16 | 17 | 18 | class get_contact_list(PluginInterface): 19 | def __init__(self): 20 | config_path = "plugins/command/get_contact_list.yml" 21 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 22 | config = yaml.safe_load(f.read()) 23 | 24 | self.excel_save_path = config["excel_save_path"] # 保存路径 25 | 26 | main_config_path = "main_config.yml" 27 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 28 | main_config = yaml.safe_load(f.read()) 29 | 30 | self.admin_list = main_config["admins"] # 获取管理员列表 31 | 32 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 33 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 34 | 35 | admin_wxid = recv.sender 36 | 37 | if admin_wxid in self.admin_list: # 判断操作人是否在管理员列表内 38 | wb = Workbook(write_only=True) 39 | xybot_contact_sheet = wb.create_sheet("XYBot通讯录") 40 | 41 | heading = ["wxid", "code", "remark", "name", "country", "province", "city", "gender"] 42 | xybot_contact_sheet.append(heading) 43 | 44 | contact_list = bot.get_contacts() 45 | 46 | for record in contact_list: # 在通讯录数据中for 47 | wxid = record["wxid"] # 获取wxid 48 | code = record["code"] # 昵称 49 | remark = record["remark"] # 微信定义的类型 50 | name = record["name"] # 自定义微信号 51 | country = record["country"] 52 | province = record["province"] 53 | city = record["city"] 54 | gender = record["gender"] 55 | 56 | xybot_contact_sheet.append([wxid, code, remark, name, country, province, city, gender]) # 加入表格 57 | 58 | excel_path = f"{self.excel_save_path}/XYBotContact_{time.time_ns()}.xlsx" # 保存路径 59 | wb.save(excel_path) # 保存表格 60 | 61 | path = os.path.abspath(excel_path) 62 | 63 | logger.info(f'[发送文件]{path}| [发送到] {recv.roomid}') # 发送 64 | bot.send_file(path, recv.roomid) # 发送文件 65 | 66 | else: # 用户不是管理员 67 | out_message = "-----XYBot-----\n❌你配用这个指令吗?" 68 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 69 | bot.send_text(recv.roomid, out_message) 70 | -------------------------------------------------------------------------------- /plugins/command/get_contact_list.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "获取机器人通讯录", "获取通讯录" , "机器人通讯录" ] 2 | plugin_name: "get_contact_list" 3 | 4 | excel_save_path: "resources/cache" -------------------------------------------------------------------------------- /plugins/command/gomoku.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "五子棋", "gomoku" ] 2 | plugin_name: "gomoku" 3 | 4 | command_format_menu: "⚙️创建游戏:\n五子棋 邀请 @xxx\n\n⚙️接受游戏:\n五子棋 接收 邀请码\n\n⚙️下棋:\n五子棋 下棋 坐标" 5 | 6 | create_game_sub_keywords: [ "创建游戏", "创建", "create","邀请","invite" ] 7 | accept_game_sub_keywords: [ "接受", "accept" ] 8 | 9 | play_sub_keywords: [ "下棋", "play", "落子" ] 10 | 11 | global_timeout: 60 # 单位为秒 -------------------------------------------------------------------------------- /plugins/command/gpt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from openai import AsyncOpenAI 10 | from wcferry import client 11 | 12 | from utils.database import BotDatabase 13 | from utils.plugin_interface import PluginInterface 14 | from wcferry_helper import XYBotWxMsg 15 | 16 | 17 | class gpt(PluginInterface): 18 | def __init__(self): 19 | config_path = "plugins/command/gpt.yml" 20 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 21 | config = yaml.safe_load(f.read()) 22 | 23 | self.command_format_menu = config["command_format_menu"] # 指令格式 24 | 25 | self.gpt_version = config["gpt_version"] # gpt版本 26 | self.gpt_point_price = config["gpt_point_price"] # gpt使用价格(单次) 27 | self.gpt_max_token = config["gpt_max_token"] # gpt 最大token 28 | self.gpt_temperature = config["gpt_temperature"] # gpt 温度 29 | 30 | main_config_path = "main_config.yml" 31 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 32 | main_config = yaml.safe_load(f.read()) 33 | 34 | self.admins = main_config["admins"] # 获取管理员列表 35 | 36 | self.openai_api_base = main_config["openai_api_base"] # openai api 链接 37 | self.openai_api_key = main_config["openai_api_key"] # openai api 密钥 38 | 39 | sensitive_words_path = "sensitive_words.yml" # 加载敏感词yml 40 | with open(sensitive_words_path, "r", encoding="utf-8") as f: # 读取设置 41 | sensitive_words_config = yaml.safe_load(f.read()) 42 | self.sensitive_words = sensitive_words_config["sensitive_words"] # 敏感词列表 43 | 44 | self.db = BotDatabase() 45 | 46 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 47 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 48 | 49 | user_wxid = recv.sender # 获取发送者wxid 50 | 51 | error_message = "" 52 | 53 | if self.db.get_points(user_wxid) < self.gpt_point_price and self.db.get_whitelist( 54 | user_wxid) != 1 and user_wxid not in self.admins: # 积分不足 不在白名单 不是管理员 55 | error_message = f"-----XYBot-----\n积分不足,需要{self.gpt_point_price}点⚠️" 56 | elif len(recv.content) < 2: # 指令格式正确 57 | error_message = f"-----XYBot-----\n参数错误!❌\n\n{self.command_format_menu}" 58 | 59 | gpt_request_message = " ".join(recv.content[1:]) # 用户问题 60 | if not self.senstitive_word_check(gpt_request_message): # 敏感词检查 61 | error_message = "-----XYBot-----\n内容包含敏感词!⚠️" 62 | 63 | if not error_message: 64 | out_message = "-----XYBot-----\n已收到指令,处理中,请勿重复发送指令!👍" # 发送已收到信息,防止用户反复发送命令 65 | await self.send_friend_or_group(bot, recv, out_message) 66 | 67 | if self.db.get_whitelist(user_wxid) == 1 or user_wxid in self.admins: # 如果用户在白名单内/是管理员 68 | chatgpt_answer = await self.chatgpt(gpt_request_message) 69 | if chatgpt_answer[0]: 70 | out_message = f"-----XYBot-----\n因为你在白名单内,所以没扣除积分!👍\nChatGPT回答:\n{chatgpt_answer[1]}\n\n⚙️ChatGPT版本:{self.gpt_version}" 71 | else: 72 | out_message = f"-----XYBot-----\n出现错误!⚠️{chatgpt_answer}" 73 | await self.send_friend_or_group(bot, recv, out_message) 74 | 75 | elif self.db.get_points(user_wxid) >= self.gpt_point_price: 76 | self.db.add_points(user_wxid, self.gpt_point_price * -1) # 减掉积分 77 | chatgpt_answer = await self.chatgpt(gpt_request_message) # 从chatgpt api 获取回答 78 | if chatgpt_answer[0]: 79 | out_message = f"-----XYBot-----\n已扣除{self.gpt_point_price}点积分,还剩{self.db.get_points(user_wxid)}点积分👍\nChatGPT回答:\n{chatgpt_answer[1]}\n\n⚙️ChatGPT版本:{self.gpt_version}" # 创建信息 80 | else: 81 | self.db.add_points(user_wxid, self.gpt_point_price) # 补回积分 82 | out_message = f"-----XYBot-----\n出现错误,已补回积分!⚠️{chatgpt_answer}" 83 | await self.send_friend_or_group(bot, recv, out_message) 84 | else: 85 | await self.send_friend_or_group(bot, recv, error_message) 86 | 87 | async def chatgpt(self, gpt_request_message): 88 | client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) 89 | try: 90 | chat_completion = await client.chat.completions.create( 91 | messages=[ 92 | { 93 | "role": "user", 94 | "content": gpt_request_message, 95 | } 96 | ], 97 | model=self.gpt_version, 98 | temperature=self.gpt_temperature, 99 | max_tokens=self.gpt_max_token, 100 | ) 101 | return True, chat_completion.choices[0].message.content 102 | except Exception as error: 103 | return False, error 104 | 105 | def senstitive_word_check(self, message): # 检查敏感词 106 | for word in self.sensitive_words: 107 | if word in message: 108 | return False 109 | return True 110 | 111 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 112 | if recv.from_group(): # 判断是群还是私聊 113 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 114 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 115 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 116 | 117 | else: 118 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 119 | bot.send_text(out_message, recv.roomid) # 发送 120 | -------------------------------------------------------------------------------- /plugins/command/gpt.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "gpt","GPT","ChatGPT","chatgpt","CHATGPT" ] 2 | plugin_name: "gpt" 3 | 4 | command_format_menu: "⚙️向AI提问:\ngpt 问题" 5 | 6 | gpt_point_price: 3 7 | 8 | gpt_version: 'gpt-4o-mini' 9 | gpt_max_token: 1000 10 | gpt_temperature: 0.5 -------------------------------------------------------------------------------- /plugins/command/hypixel_info.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | # 5 | # This program is licensed under the GNU General Public License v3.0. 6 | 7 | import asyncio 8 | import re 9 | 10 | import aiohttp 11 | import yaml 12 | from bs4 import BeautifulSoup 13 | from loguru import logger 14 | from wcferry import client 15 | 16 | from utils.database import BotDatabase 17 | from utils.plugin_interface import PluginInterface 18 | from wcferry_helper import XYBotWxMsg 19 | 20 | 21 | class hypixel_info(PluginInterface): 22 | def __init__(self): 23 | config_path = "plugins/command/hypixel_info.yml" 24 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 25 | config = yaml.safe_load(f.read()) 26 | 27 | self.bedwar_keywords = config["bedwar_keywords"] # 获取查询bedwar小游戏关键词 28 | 29 | self.db = BotDatabase() 30 | 31 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 32 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 33 | 34 | bot.send_text("请注意,hypixel_info插件已不再进行维护", recv.roomid) 35 | 36 | headers = { 37 | "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE" 38 | } # 设置user agent 绕cf 39 | 40 | # 指令格式错误判断 41 | if len(recv.content) == 1 or len(recv.content) > 3: 42 | out_message = "-----XYBot-----\n格式错误❌" 43 | 44 | await self.send_friend_or_group(bot, recv, out_message) 45 | 46 | elif len(recv.content) == 2: # Basic info 47 | await asyncio.create_task(self.send_basic_info(bot, recv, headers)) 48 | 49 | elif len(recv.content) == 3: 50 | if recv.content[1] in self.bedwar_keywords: # bedwar 51 | await asyncio.create_task(self.send_bedwar_info(bot, recv, headers)) 52 | 53 | else: 54 | out_message = "-----XYBot-----\n不存在的游戏!❌" 55 | await self.send_friend_or_group(bot, recv, out_message) 56 | 57 | @staticmethod 58 | def check_valid(soup): 59 | for i in soup.find_all("h3", {"class": "m-t-0 header-title"}): 60 | if "Player Information" in i.get_text(): 61 | return True 62 | return False 63 | 64 | @staticmethod 65 | def get_in_game_name(soup): # 获取玩家游戏内名字 in game name 66 | # ign 67 | in_game_name = ( 68 | soup.find("div", id="wrapper") 69 | .find("span", {"style": "font-family: 'Minecraftia', serif;"}) 70 | .text 71 | ) # 爬虫查询,用css格式 72 | return in_game_name 73 | 74 | @staticmethod 75 | def get_basic_stats(soup): 76 | basic_stats = {} 77 | stats_bs4 = ( 78 | soup.find("div", id="wrapper") 79 | .find_all("div", {"class": "card-box m-b-10"})[0] 80 | .find_all("b")[:-1] 81 | ) # 爬虫查询,用css格式 82 | for stat in stats_bs4: # 从爬到的数据中提取 83 | basic_stats[stat.get_text() + " "] = ( 84 | stat.next_sibling.strip() 85 | ) # 处理文本,去除空格特殊符号等 86 | return basic_stats 87 | 88 | @staticmethod 89 | def get_guild_stat(soup): 90 | # guild 91 | guild_stat = {} 92 | guild_bs4 = soup.find("div", id="wrapper").find_all( 93 | "div", {"class": "card-box m-b-10"} 94 | )[ 95 | 1 96 | ] # 爬虫查询,用css格式 97 | if "Guild" in guild_bs4.get_text(): # 处理是否在公会中 98 | for info in guild_bs4.find_all("b"): # 从爬到的数据中提取 99 | guild_stat[info.get_text().strip() + " "] = info.next_sibling.get_text( 100 | separator="\n" 101 | ) # 处理文本,去除空格特殊符号等 102 | return guild_stat 103 | 104 | @staticmethod 105 | def get_status(soup): 106 | # status 107 | status = {} 108 | status_bs4 = soup.find("div", id="wrapper").find_all( 109 | "div", {"class": "card-box m-b-10"} 110 | ) # 爬虫查询,用css格式 111 | for i in status_bs4: # 遍历查询结果 112 | if "Status" in i.get_text(): # 判断是否在线 113 | if "Offline" in i.get_text(): 114 | status["Status: "] = "Offline" 115 | 116 | return status 117 | else: 118 | status["Status: "] = "Online" 119 | for info in i.find_all("b"): 120 | status[info.get_text().strip() + ": "] = ( 121 | info.next_sibling.get_text() 122 | ) 123 | 124 | return status 125 | 126 | @staticmethod 127 | def get_bedwar_stat(soup): 128 | # bw 129 | bw_stat = [] 130 | table = soup.find("div", id="stat_panel_BedWars").find( 131 | "table", {"class": "table"} 132 | ) # 爬虫查询,用css格式 133 | for row in table.find_all("tr")[2:]: # 遍历搜到的结果 134 | row_info_list = row.get_text(separator="#").split("#") # 处理文本,去处# 135 | if row_info_list[0]: # 判断结果是否有效 136 | bw_stat.append(row_info_list) 137 | return bw_stat 138 | 139 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 140 | if recv.from_group(): # 判断是群还是私聊 141 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 142 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 143 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 144 | else: 145 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 146 | bot.send_text(out_message, recv.roomid) # 发送 147 | 148 | async def send_basic_info(self, bot, recv, headers): 149 | request_ign = recv.content[1] # 请求的玩家ign (游戏内名字 in game name) 150 | 151 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n查询玩家 {request_ign} 中,请稍候!🙂") 152 | 153 | conn_ssl = aiohttp.TCPConnector(verify_ssl=False) 154 | async with aiohttp.request( 155 | "GET", 156 | url=f"http://plancke.io/hypixel/player/stats/{request_ign}", 157 | headers=headers, 158 | connector=conn_ssl, 159 | ) as req: 160 | soup = BeautifulSoup(await req.text(), "html.parser") 161 | await conn_ssl.close() 162 | 163 | if req.status != 404 and self.check_valid(soup): 164 | 165 | # basic info 166 | in_game_name = self.get_in_game_name(soup) 167 | basic_stats = self.get_basic_stats(soup) 168 | guild_stat = self.get_guild_stat(soup) 169 | status = self.get_status(soup) 170 | 171 | # 组建消息 172 | out_message = f"-----XYBot-----\n🎮玩家:\n{in_game_name}\n\n--------\n\n⚙️基础信息:\n" 173 | for key, value in basic_stats.items(): 174 | out_message = out_message + key + value + "\n" 175 | out_message += "\n--------\n\n🏹公会信息:\n" 176 | for key, value in guild_stat.items(): 177 | out_message = out_message + key + value + "\n" 178 | out_message += "\n--------\n\nℹ️当前状态:\n" 179 | for key, value in status.items(): 180 | out_message = out_message + key + value + "\n" 181 | 182 | # 发送消息 183 | await self.send_friend_or_group(recv, out_message) 184 | 185 | else: # 玩家不存在 186 | out_message = f"-----XYBot-----\n玩家 {request_ign} 不存在!❌" 187 | await self.send_friend_or_group(bot, recv, out_message) 188 | 189 | async def send_bedwar_info(self, bot, recv, headers): # 获取玩家bedwar信息 190 | request_ign = recv.content[2] # 请求的玩家ign (游戏内名字 in game name) 191 | 192 | await self.send_friend_or_group(bot, recv, 193 | f"-----XYBot-----\n查询玩家 {request_ign} 中,请稍候!🙂") # 发送查询确认,让用户等待 194 | 195 | conn_ssl = aiohttp.TCPConnector(verify_ssl=False) 196 | async with aiohttp.request( 197 | "GET", 198 | url=f"http://plancke.io/hypixel/player/stats/{request_ign}", 199 | headers=headers, 200 | connector=conn_ssl, 201 | ) as req: 202 | soup = BeautifulSoup(await req.text(), "html.parser") 203 | await conn_ssl.close() 204 | 205 | if req.status != 404 and self.check_valid(soup): # 判断响应是否有效 206 | 207 | in_game_name = self.get_in_game_name(soup) # 从爬虫获取玩家真实ign 208 | bedwar_stat = self.get_bedwar_stat(soup) # 从爬虫获取玩家bedwar信息 209 | 210 | # 组建信息 211 | out_message = f"-----XYBot-----\n🎮玩家:\n{in_game_name}\n\n--------\n\n🛏️起床战争信息:\n" 212 | table_header = [ 213 | "⚔️模式:", 214 | "击杀:", 215 | "死亡:", 216 | "K/D:", 217 | "最终击杀:", 218 | "最终死亡:", 219 | "最终K/D:", 220 | "胜利:", 221 | "失败:", 222 | "W/L:", 223 | "破坏床数:", 224 | ] 225 | for row in bedwar_stat: 226 | for cell in range(len(row)): 227 | out_message = out_message + table_header[cell] + row[cell] + "\n" 228 | out_message += "\n" 229 | 230 | # 发送 231 | await self.send_friend_or_group(bot, recv, out_message) 232 | else: # 玩家不存在 233 | out_message = f"-----XYBot-----\n玩家 {request_ign} 不存在!❌" 234 | await self.send_friend_or_group(bot, recv, out_message) 235 | -------------------------------------------------------------------------------- /plugins/command/hypixel_info.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "hyp","HYP","Hyp","hypixel","Hypixel", "Hypixel玩家查询" ] 2 | plugin_name: "hypixel_info" 3 | 4 | bedwar_keywords: [ "bw","BW","bedwar","BEDWAR","BedWar" ] -------------------------------------------------------------------------------- /plugins/command/lucky_draw.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import random 6 | import re 7 | 8 | import yaml 9 | from loguru import logger 10 | from wcferry import client 11 | 12 | from utils.database import BotDatabase 13 | from utils.plugin_interface import PluginInterface 14 | from wcferry_helper import XYBotWxMsg 15 | 16 | 17 | class lucky_draw(PluginInterface): 18 | def __init__(self): 19 | config_path = "plugins/command/lucky_draw.yml" 20 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 21 | config = yaml.safe_load(f.read()) 22 | 23 | self.command_format_menu = config["command_format_menu"] # 命令格式 24 | 25 | self.lucky_draw_probability = config["lucky_draw_probability"] # 抽奖概率 26 | self.max_draw = config["max_draw"] # 连抽最大数量 27 | self.draw_per_guarantee = config[ 28 | "draw_per_guarantee" 29 | ] # 保底抽奖次数 每个保底需要x抽 30 | self.guaranteed_max_probability = config["guaranteed_max_probability"] 31 | 32 | self.db = BotDatabase() # 实例化数据库类 33 | 34 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 35 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 36 | 37 | global _draw_count, _draw_name # 全局变量防止出错 38 | 39 | # -----初始化与消息格式监测----- 40 | target_wxid = recv.sender # 获取发送者wxid 41 | 42 | command = recv.content # 指令 43 | 44 | target_points = self.db.get_points(target_wxid) # 获取目标积分 45 | 46 | error = "" 47 | 48 | if len(command) == 2: # 判断指令格式 49 | _draw_name = command[1] # 抽奖名 50 | _draw_count = 1 # 抽奖次数,单抽设为1 51 | 52 | if ( 53 | _draw_name not in self.lucky_draw_probability.keys() 54 | ): # 判断抽奖是否有效,积分是否够 55 | error = "-----XYBot-----\n❌抽奖种类未知或者无效" 56 | elif ( 57 | _draw_name in self.lucky_draw_probability.keys() 58 | and target_points < self.lucky_draw_probability[_draw_name]["cost"] 59 | ): 60 | error = "-----XYBot-----\n❌积分不足!" 61 | 62 | elif len(command) == 3 and command[2].isdigit(): 63 | _draw_name = command[1] 64 | _draw_count = int(command[2]) 65 | 66 | if ( 67 | _draw_name not in self.lucky_draw_probability.keys() 68 | ): # 判断抽奖是否有效,积分是否够,连抽要乘次数 69 | error = "-----XYBot-----\n❌抽奖种类未知或者无效" 70 | elif ( 71 | _draw_name in self.lucky_draw_probability.keys() 72 | and target_points 73 | < self.lucky_draw_probability[_draw_name]["cost"] * _draw_count 74 | ): 75 | error = "-----XYBot-----\n❌积分不足!" 76 | else: # 指令格式错误 77 | error = f"-----XYBot-----\n❌命令格式错误!\n\n{self.command_format_menu}" 78 | 79 | if not error: 80 | 81 | # -----抽奖核心部分----- 82 | 83 | draw_probability = self.lucky_draw_probability[_draw_name][ 84 | "probability" 85 | ] # 从设置获取抽奖名概率 86 | draw_cost = ( 87 | self.lucky_draw_probability[_draw_name]["cost"] * _draw_count 88 | ) # 从设置获取抽奖消耗积分 89 | 90 | wins = [] # 赢取列表 91 | 92 | self.db.add_points(target_wxid, -1 * draw_cost) # 扣取积分 93 | 94 | # 保底抽奖 95 | min_guaranteed = _draw_count // self.draw_per_guarantee # 保底抽奖次数 96 | for _ in range(min_guaranteed): # 先把保底抽了 97 | random_num = random.uniform(0, self.guaranteed_max_probability) 98 | cumulative_probability = 0 99 | for probability, prize_dict in draw_probability.items(): 100 | cumulative_probability += float(probability) 101 | if random_num <= cumulative_probability: 102 | win_name = prize_dict["name"] 103 | win_points = prize_dict["points"] 104 | win_symbol = prize_dict["symbol"] 105 | 106 | wins.append( 107 | (win_name, win_points, win_symbol) 108 | ) # 把结果加入赢取列表 109 | break 110 | 111 | # 正常抽奖 112 | for _ in range(_draw_count - min_guaranteed): # 把剩下的抽了 113 | random_num = random.uniform(0, 1) 114 | cumulative_probability = 0 115 | for probability, prize_dict in draw_probability.items(): 116 | cumulative_probability += float(probability) 117 | if random_num <= cumulative_probability: 118 | win_name = prize_dict["name"] 119 | win_points = prize_dict["points"] 120 | win_symbol = prize_dict["symbol"] 121 | 122 | wins.append( 123 | (win_name, win_points, win_symbol) 124 | ) # 把结果加入赢取列表 125 | break 126 | 127 | # -----消息组建----- 128 | 129 | total_win_points = 0 130 | for win_name, win_points, win_symbol in wins: # 统计赢取的积分 131 | total_win_points += win_points 132 | 133 | self.db.add_points(target_wxid, total_win_points) # 把赢取的积分加入数据库 134 | logger.info( 135 | f"[抽奖] wxid: {target_wxid} | 抽奖名: {_draw_name} | 次数: {_draw_count} | 赢取积分: {total_win_points}" 136 | ) 137 | 138 | message = self.make_message( 139 | wins, _draw_name, _draw_count, total_win_points, draw_cost 140 | ) # 组建信息 141 | 142 | await self.send_friend_or_group(bot, recv, message) # 发送 143 | bot.send_pat_msg(recv.roomid, target_wxid) # 发送拍一拍消息 144 | 145 | else: 146 | await self.send_friend_or_group(bot, recv, error) # 发送错误 147 | 148 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 149 | if recv.from_group(): # 判断是群还是私聊 150 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 151 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 152 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 153 | else: 154 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 155 | bot.send_text(out_message, recv.roomid) # 发送 156 | 157 | @staticmethod 158 | def make_message( 159 | wins, _draw_name, _draw_count, total_win_points, draw_cost 160 | ): # 组建信息 161 | name_max_len = 0 162 | for win_name, win_points, win_symbol in wins: 163 | if len(win_name) > name_max_len: 164 | name_max_len = len(win_name) 165 | 166 | begin_message = f"----XYBot抽奖----\n🥳恭喜你在 {_draw_count}次 {_draw_name}抽奖 中抽到了:\n\n" 167 | lines = [] 168 | for _ in range(name_max_len + 2): 169 | lines.append("") 170 | 171 | begin_line = 0 172 | 173 | one_line_length = 0 174 | 175 | for win_name, win_points, win_symbol in wins: 176 | if one_line_length >= 10: # 每行10个结果,以免在微信上格式错误 177 | begin_line += name_max_len + 2 178 | for _ in range(name_max_len + 2): 179 | lines.append("") # 占个位 180 | one_line_length = 0 181 | 182 | lines[begin_line] += win_symbol 183 | for i in range(begin_line + 1, begin_line + name_max_len + 1): 184 | if i % (name_max_len + 2) <= len(win_name): 185 | lines[i] += ( 186 | "\u2004" + win_name[i % (name_max_len + 2) - 1] 187 | ) # \u2004 这个空格最好 试过了很多种空格 188 | else: 189 | lines[i] += win_symbol 190 | lines[begin_line + name_max_len + 1] += win_symbol 191 | 192 | one_line_length += 1 193 | 194 | message = begin_message 195 | for line in lines: 196 | message += line + "\n" 197 | 198 | message += f"\n\n🎉总计赢取积分: {total_win_points}🎉\n🎉共计消耗积分:{draw_cost}🎉\n\n概率请自行查询菜单⚙️" 199 | 200 | return message 201 | -------------------------------------------------------------------------------- /plugins/command/lucky_draw.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "抽奖","积分抽奖" ] 2 | plugin_name: "lucky_draw" 3 | 4 | command_format_menu: "⚙️抽奖:\n抽奖 小|中|大 次数" 5 | 6 | max_draw: 100 7 | 8 | draw_per_guarantee: 10 9 | guaranteed_max_probability: 0.4 10 | 11 | lucky_draw_probability: { 12 | "小": { "cost": 20, "probability": { "0.05": { "name": "金","points": 40, "symbol": "🟨" }, 13 | "0.1": { "name": "紫","points": 35, "symbol": "🟪" }, 14 | "0.2": { "name": "蓝","points": 21, "symbol": "🟦" }, 15 | "0.3": { "name": "绿","points": 15, "symbol": "🟩" }, 16 | "0.35": { "name": "白","points": 10, "symbol": "⬜️" } } 17 | }, 18 | "中": { "cost": 40, "probability": { "0.05": { "name": "金","points": 70, "symbol": "🟨" }, 19 | "0.1": { "name": "紫","points": 55, "symbol": "🟪" }, 20 | "0.2": { "name": "蓝","points": 41, "symbol": "🟦" }, 21 | "0.3": { "name": "绿","points": 35, "symbol": "🟩" }, 22 | "0.35": { "name": "白","points": 25, "symbol": "⬜️" } } 23 | }, 24 | "大": { "cost": 80, "probability": { "0.01": { "name": "红", "points": 170, "symbol": "🟥" }, 25 | "0.05": { "name": "金","points": 120, "symbol": "🟨" }, 26 | "0.1": { "name": "紫","points": 90, "symbol": "🟪" }, 27 | "0.2": { "name": "蓝","points": 81, "symbol": "🟦" }, 28 | "0.3": { "name": "绿","points": 75, "symbol": "🟩" }, 29 | "0.34": { "name": "白","points": 65, "symbol": "⬜️" } } 30 | } 31 | } -------------------------------------------------------------------------------- /plugins/command/manage_plugins.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.plugin_interface import PluginInterface 12 | from utils.plugin_manager import plugin_manager 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class manage_plugins(PluginInterface): 17 | def __init__(self): 18 | config_path = "plugins/command/manage_plugins.yml" 19 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 20 | config = yaml.safe_load(f.read()) 21 | 22 | self.command_format_menu = config['command_format_menu'] 23 | 24 | self.load_sub_keywords = config['load_sub_keywords'] 25 | self.unload_sub_keywords = config['unload_sub_keywords'] 26 | self.reload_sub_keywords = config['reload_sub_keywords'] 27 | self.list_sub_keywords = config['list_sub_keywords'] 28 | 29 | main_config_path = "main_config.yml" 30 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 31 | main_config = yaml.safe_load(f.read()) 32 | 33 | self.admin_list = main_config["admins"] # 获取管理员列表 34 | 35 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 36 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 37 | 38 | admin_wxid = recv.sender # 获取发送者wxid 39 | 40 | if admin_wxid in self.admin_list: # 判断是否为管理员 41 | action = recv.content[1] # 获取操作名 42 | 43 | if action in self.load_sub_keywords: # 如果操作为加载,则调用插件管理器加载插件 44 | await self.load_plugin(bot, recv) 45 | 46 | elif action in self.unload_sub_keywords: # 如果操作为卸载,则调用插件管理器卸载插件 47 | await self.unload_plugin(bot, recv) 48 | 49 | elif action in self.reload_sub_keywords: # 如果操作为重载,则调用插件管理器重载插件 50 | await self.reload_plugin(bot, recv) 51 | 52 | elif action in self.list_sub_keywords: # 如果操作为列表,则调用插件管理器获取插件列表 53 | await self.list_plugins(bot, recv) 54 | 55 | else: # 操作不存在,则响应错误 56 | out_message = f"-----XYBot-----\n⚠️该操作不存在!\n\n{self.command_format_menu}" 57 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 58 | bot.send_text(out_message, recv.roomid) 59 | 60 | 61 | else: # 操作人不在白名单内 62 | out_message = "-----XYBot-----\n❌你配用这个指令吗?" 63 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 64 | bot.send_text(out_message, recv.roomid) 65 | 66 | async def load_plugin(self, bot: client.Wcf, recv: XYBotWxMsg): 67 | try: 68 | action_plugin = recv.content[2] # 获取插件名 69 | 70 | if action_plugin == 'manage_plugins': 71 | out_message = "-----XYBot-----\n❌不能加载该插件!" 72 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 73 | bot.send_text(out_message, recv.roomid) 74 | 75 | elif action_plugin == '*': 76 | status = plugin_manager.load_plugins() 77 | if status[0]: # 判断是否成功并发送响应 78 | out_message = f"-----XYBot-----\n加载所有插件成功!✅\n{status[1]}" 79 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 80 | bot.send_text(out_message, recv.roomid) 81 | else: 82 | out_message = f"-----XYBot-----\n加载所有插件失败!❌\n{status[1]}" 83 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 84 | bot.send_text(out_message, recv.roomid) 85 | 86 | else: 87 | status = plugin_manager.load_plugin(action_plugin) # 判断是否成功并发送响应 88 | if status[0]: # 判断是否成功并发送响应 89 | out_message = f"-----XYBot-----\n加载插件{action_plugin}成功!✅\n{status[1]}" 90 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 91 | bot.send_text(out_message, recv.roomid) 92 | else: 93 | out_message = f"-----XYBot-----\n加载插件{action_plugin}失败!❌\n{status[1]}" 94 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 95 | bot.send_text(out_message, recv.roomid) 96 | 97 | except Exception as error: 98 | out_message = f"-----XYBot-----\n加载插件失败!❌\n{error}" 99 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 100 | bot.send_text(out_message, recv.roomid) 101 | 102 | async def unload_plugin(self, bot: client.Wcf, recv: XYBotWxMsg): 103 | try: 104 | action_plugin = recv.content[2] # 获取插件名 105 | 106 | if action_plugin == 'manage_plugins': 107 | out_message = "-----XYBot-----\n❌不能卸载该插件!" 108 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 109 | bot.send_text(out_message, recv.roomid) 110 | 111 | elif action_plugin == '*': 112 | status = plugin_manager.unload_plugins() 113 | if status[0]: # 判断是否成功并发送响应 114 | out_message = f"-----XYBot-----\n卸载所有插件成功!✅\n{status[1]}" 115 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 116 | bot.send_text(out_message, recv.roomid) 117 | else: 118 | out_message = f"-----XYBot-----\n卸载所有插件失败!❌\n{status[1]}" 119 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 120 | bot.send_text(out_message, recv.roomid) 121 | 122 | else: 123 | status = plugin_manager.unload_plugin(action_plugin) 124 | if status[0]: # 判断是否成功并发送响应 125 | out_message = f"-----XYBot-----\n卸载插件{action_plugin}成功!✅\n{status[1]}" 126 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 127 | bot.send_text(out_message, recv.roomid) 128 | 129 | else: 130 | out_message = f"-----XYBot-----\n卸载插件{action_plugin}失败!❌\n{status[1]}" 131 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 132 | bot.send_text(out_message, recv.roomid) 133 | 134 | except Exception as error: 135 | out_message = f"-----XYBot-----\n卸载插件失败!❌\n{error}" 136 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 137 | bot.send_text(out_message, recv.roomid) 138 | 139 | async def reload_plugin(self, bot: client.Wcf, recv: XYBotWxMsg): 140 | try: 141 | action_plugin = recv.content[2] # 获取插件名 142 | 143 | if action_plugin == 'manage_plugins': 144 | out_message = "-----XYBot-----\n❌不能重载该插件!" 145 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 146 | bot.send_text(out_message, recv.roomid) 147 | 148 | elif action_plugin == '*': 149 | status = plugin_manager.reload_plugins() 150 | if status[0]: 151 | out_message = f"-----XYBot-----\n重载所有插件成功!✅\n{status[1]}" 152 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 153 | bot.send_text(out_message, recv.roomid) 154 | else: 155 | out_message = f"-----XYBot-----\n重载所有插件失败!❌\n{status[1]}" 156 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 157 | bot.send_text(out_message, recv.roomid) 158 | 159 | else: 160 | status = plugin_manager.reload_plugin(action_plugin) 161 | if status[0]: # 判断是否成功并发送响应 162 | out_message = f"-----XYBot-----\n重载插件{action_plugin}成功!✅\n{status[1]}" 163 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 164 | bot.send_text(out_message, recv.roomid) 165 | else: 166 | out_message = f"-----XYBot-----\n重载插件{action_plugin}失败!❌\n{status[1]}" 167 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 168 | bot.send_text(out_message, recv.roomid) 169 | 170 | except Exception as error: 171 | out_message = f"-----XYBot-----\n重载插件失败!❌\n{error}" 172 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 173 | bot.send_text(out_message, recv.roomid) 174 | 175 | async def list_plugins(self, bot: client.Wcf, recv: XYBotWxMsg): 176 | out_message = "-----XYBot-----\n已加载插件列表:" 177 | for type in plugin_manager.all_plugin_types: 178 | for plugin in plugin_manager.plugins[type]: 179 | out_message += f"\n{plugin}" 180 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 181 | bot.send_text(out_message, recv.roomid) 182 | -------------------------------------------------------------------------------- /plugins/command/manage_plugins.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "管理插件" ] 2 | plugin_name: "manage_plugins" 3 | 4 | command_format_menu: "⚙️加载插件:\n管理插件 加载 插件名(英文)\n\n⚙️卸载插件:\n管理插件 卸载 插件名(英文)\n\n⚙️重载插件:\n管理插件 重载 插件名(英文)\n\n⚙️列出插件:\n管理插件 列表" 5 | 6 | load_sub_keywords: [ "加载", "load" ] 7 | unload_sub_keywords: [ "卸载", "unload" ] 8 | reload_sub_keywords: [ "重载", "reload" ] 9 | list_sub_keywords: [ "列表", "list" ] -------------------------------------------------------------------------------- /plugins/command/menu.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.plugin_interface import PluginInterface 12 | from wcferry_helper import XYBotWxMsg 13 | 14 | 15 | class menu(PluginInterface): 16 | def __init__(self): 17 | config_path = "plugins/command/menu.yml" 18 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 19 | config = yaml.safe_load(f.read()) 20 | 21 | self.main_menu = config["main_menu"] 22 | self.menus = config["menus"] 23 | 24 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 25 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 26 | 27 | if len(recv.content) == 1: # 如果命令列表长度为1,那就代表请求主菜单 28 | out_message = self.main_menu 29 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 30 | bot.send_text(out_message, recv.roomid) 31 | 32 | elif recv.content[1] in self.menus.keys(): # 长度不为1,发送以参数为键菜单内容为值的字典 33 | out_message = self.menus[recv.content[1]] 34 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 35 | bot.send_text(out_message, recv.roomid) 36 | 37 | else: 38 | out_message = "找不到此菜单!⚠️" # 没找到对应菜单,发送未找到 39 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 40 | bot.send_text(out_message, recv.roomid) 41 | -------------------------------------------------------------------------------- /plugins/command/menu.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "菜单", "menu", "cd" ] 2 | plugin_name: "menu" 3 | 4 | main_menu: "-----XYBot菜单------\n实用功能⚙️\n1.1 获取天气\n1.2 获取新闻\n1.3 ChatGPT\n1.4 AI绘图\n1.5 战争雷霆玩家查询\n1.6 选外卖\n\n娱乐功能🔥\n2.1 随机图图\n2.2 随机链接\n2.3 五子棋\n\n积分功能💰\n3.1 签到\n3.2 查询积分\n3.3 积分榜\n3.4 积分转送\n3.5 积分抽奖\n3.6 积分红包\n\n🔧管理员功能\n4.1 管理员菜单\n\n获取菜单指令格式: 菜单 编号\n例如:菜单 1.1" 5 | 6 | menus: { 7 | "1.1": "-----XYBot菜单------\n1.1: 获取最新全球实时天气🌧️\n指令:获取天气 城市", 8 | "1.2": "-----XYBot菜单------\n1.2: 获取最新头条新闻📰\n指令: 新闻", 9 | "1.3": "-----XYBot菜单------\n1.3: 在微信中用ChatGPT🤖️\n(支持私聊)\n\n⚙️ChatGPT3.5指令:\ngpt3 问题\n⚠️注意!扣除3点积分!⚠️\n\n⚙️ChatGPT4指令:\ngpt4 问题\n⚠️注意!扣除10点积分!⚠️\n❗️请注意GPT4价格贵,请勿滥用!❗️\n\n在设置中开启私聊 ChatGPT 后,可以在机器人私信直接问问题,不需要指令,还支持上下文关联!🎉", 10 | "1.4": "-----XYBot菜单------\n1.4: AI绘图,让AI画画!✍️\n指令:AI绘图 描述\n如:AI绘图 像素风,一只可爱的猫在草地上玩耍", 11 | "1.5": "-----XYBot菜单------\n1.5:查询战争雷霆玩家信息!🛩️\n\n⚙️指令:战雷查询 玩家名字", 12 | "1.6": "-----XYBot菜单------\n1.6: 选外卖\n\n不知道今天吃什么?\n🍽随机一个外卖吧!\n\n⚙️指令:\n选外卖", 13 | "2.1": "-----XYBot菜单------\n2.1: 随机一个二次元图片!\n指令:随机图图", 14 | "2.2": "-----XYBot菜单------\n2.2: 更隐蔽的随机图图!\n指令:随机链接", 15 | "2.3": "-----XYBot菜单------\n2.3: 在群聊中下五子棋!🎉\n\n⚙️邀请朋友下棋:\n五子棋 邀请 @你的朋友\n如:五子棋 邀请 @XYBot\n\n⚙️接受来自群友的邀请:\n五子棋 接受 五子棋游戏id\n如:五子棋 接受 ABC123\n⚠️五子棋游戏id在邀请时会显示\n\n⚙️在棋盘上落子:\n五子棋 下棋 横坐标纵坐标\n⚠️横坐标与纵坐标在棋盘上会显示,先字母,再数字\n如:五子棋 下棋 C5", 16 | "3.1": "-----XYBot菜单------\n3.1: 签到领取积分\n⚠️每天只能领取一次\n指令:签到", 17 | "3.2": "-----XYBot菜单------\n3.2: 查询自己的积分!\n指令:查询积分", 18 | "3.3": "-----XYBot菜单------\n3.3: 查看积分排行榜!\n指令:积分榜", 19 | "3.4": "-----XYBot菜单------\n3.4: 转送积分给其他人!👍🏻\n指令:积分转账 @群成员 积分数量\n如:积分转账 50 @XYBot", 20 | "3.5": "-----XYBot菜单------\n3.5: 使用积分抽奖,赚取积分💰\n单抽指令:抽奖 抽奖名\n连抽:抽奖 奖池名 次数\n\n有三种奖池,概率如下\n\n小 ❗️需要20点积分❗\n指令:抽奖 小 次数\n🟨金🟨 5% 40积分\n🟪紫🟪 10% 35积分\n🟦蓝🟦 20% 21积分\n🟩绿🟩 30% 15积分\n⬜️白⬜️ 35% 10积分\n\n中 ❗️需要40点积分❗️\n指令:抽奖 中 次数\n🟨金🟨 5% 70积分\n🟪紫🟪 10% 55积分\n🟦蓝🟦 20% 41积分\n🟩绿🟩 30% 35积分\n⬜️白⬜️ 35% 25积分\n\n大 ❗️需要80点积分❗️\n指令:抽奖 大 次数\n🟥红🟥 1% 170积分\n🟨金🟨 5% 120积分\n🟪紫🟪 10% 90积分\n🟦蓝🟦 20% 81积分\n🟩绿🟩 30% 75积分\n⬜️白⬜️ 34% 65积分\n\n保底:只有连抽有保底,每10抽必出🟦及以上的奖项", 21 | "3.6": "-----XYBot菜单------\n3.6: 积分红包!🧧\n\n⚙️发红包指令:发红包 积分数 红包数\n\n⚙️抢红包指令:抢红包 验证码", 22 | "4.1": "-----XYBot菜单------\n4.1: 管理员功能:\n\n无管理员也可以用的指令:\n检查机器人状态\n指令:机器人状态\n\n必须有管理员才可使用下列指令\n\n管理积分:\n指令:管理积分 @ 加/减 积分数\n如: 管理积分 @XYBot 加 10\n管理积分 @XYBot 减 10\n\n管理白名单:\n有白名单者使用ChatGPT不扣积分\n指令: 管理白名单 @ 加入/删除\n如: 管理白名单 @XYBot 加入\n管理白名单 @XYBot 删除\n\n重置签到冷却:\n指令: 重置签到状态\n无参数\n\n获取机器人通讯录功能:\n指令: 获取机器人通讯录\n无参数\n\n查看已加载插件列表:\n指令:管理插件 列表\n\n热加载/卸载/重载插件\n指令:管理插件 操作名 插件名\n例如:管理插件 重载 lucky_draw\n\n如需批量管理插件 可以用 * 代替插件名称\n* 表示了所有插件(受保护的manage_plugins除外)", 23 | "天气": "-----XYBot菜单------\n1.1: 获取最新全球实时天气🌧️\n指令:获取天气 城市", 24 | "新闻": "-----XYBot菜单------\n1.2: 获取最新头条新闻📰\n指令: 新闻", 25 | "chatgpt": "-----XYBot菜单------\n1.3: 在微信中用ChatGPT🤖️\n(支持私聊)\n\n⚙️ChatGPT3.5指令:\ngpt3 问题\n⚠️注意!扣除3点积分!⚠️\n\n⚙️ChatGPT4指令:\ngpt4 问题\n⚠️注意!扣除10点积分!⚠️\n❗️请注意GPT4价格贵,请勿滥用!❗️\n\n在设置中开启私聊 ChatGPT 后,可以在机器人私信直接问问题,不需要指令,还支持上下文关联!🎉", 26 | "AI绘图": "-----XYBot菜单------\n1.4: AI绘图,让AI画画!✍️\n指令:AI绘图 描述\n如:AI绘图 像素风,一只可爱的猫在草地上玩耍", 27 | "战争雷霆玩家查询": "-----XYBot菜单------\n1.5:查询战争雷霆玩家信息!🛩️\n\n⚙️指令:战雷查询 玩家名字", 28 | "选外卖": "-----XYBot菜单------\n1.6: 选外卖\n\n不知道今天吃什么?\n🍽随机一个外卖吧!\n\n⚙️指令:\n选外卖", 29 | "随机图图": "-----XYBot菜单------\n2.1: 随机一个二次元图片!\n指令:随机图图", 30 | "随机链接": "-----XYBot菜单------\n2.2: 更隐蔽的随机图图!\n指令:随机链接", 31 | "五子棋": "-----XYBot菜单------\n2.3: 在群聊中下五子棋!🎉\n\n⚙️邀请朋友下棋:\n五子棋 邀请 @你的朋友\n如:五子棋 邀请 @XYBot\n\n⚙️接受来自群友的邀请:\n五子棋 接受 五子棋游戏id\n如:五子棋 接受 ABC123\n⚠️五子棋游戏id在邀请时会显示\n\n⚙️在棋盘上落子:\n五子棋 下棋 横坐标纵坐标\n⚠️横坐标与纵坐标在棋盘上会显示,先字母,再数字\n如:五子棋 下棋 C5", 32 | "签到": "-----XYBot菜单------\n3.1: 签到领取积分\n⚠️每天只能领取一次\n指令:签到", 33 | "查询积分": "-----XYBot菜单------\n3.2: 查询自己的积分!\n指令:查询积分", 34 | "积分榜": "-----XYBot菜单------\n3.3: 查看积分排行榜!\n指令:积分榜", 35 | "积分转账": "-----XYBot菜单------\n3.4: 转送积分给其他人!👍🏻\n指令:积分转账 @群成员 积分数量\n如:积分转账 50 @XYBot", 36 | "抽奖": "-----XYBot菜单------\n3.5: 使用积分抽奖,赚取积分💰\n单抽指令:抽奖 抽奖名\n连抽:抽奖 奖池名 次数\n\n有三种奖池,概率如下\n\n小 ❗️需要20点积分❗\n指令:抽奖 小 次数\n🟨金🟨 5% 40积分\n🟪紫🟪 10% 35积分\n🟦蓝🟦 20% 21积分\n🟩绿🟩 30% 15积分\n⬜️白⬜️ 35% 10积分\n\n中 ❗️需要40点积分❗️\n指令:抽奖 中 次数\n🟨金🟨 5% 70积分\n🟪紫🟪 10% 55积分\n🟦蓝🟦 20% 41积分\n🟩绿🟩 30% 35积分\n⬜️白⬜️ 35% 25积分\n\n大 ❗️需要80点积分❗️\n指令:抽奖 大 次数\n🟥红🟥 1% 170积分\n🟨金🟨 5% 120积分\n🟪紫🟪 10% 90积分\n🟦蓝🟦 20% 81积分\n🟩绿🟩 30% 75积分\n⬜️白⬜️ 34% 65积分\n\n保底:只有连抽有保底,每10抽必出🟦及以上的奖项", 37 | "积分红包": "-----XYBot菜单------\n3.6: 积分红包!🧧\n\n⚙️发红包指令:发红包 积分数 红包数\n\n⚙️抢红包指令:抢红包 验证码", 38 | "管理员菜单": "-----XYBot菜单------\n4.1: 管理员功能:\n\n无管理员也可以用的指令:\n检查机器人状态\n指令:机器人状态\n\n必须有管理员才可使用下列指令\n\n管理积分:\n指令:管理积分 @ 加/减 积分数\n如: 管理积分 @XYBot 加 10\n管理积分 @XYBot 减 10\n\n管理白名单:\n有白名单者使用ChatGPT不扣积分\n指令: 管理白名单 @ 加入/删除\n如: 管理白名单 @XYBot 加入\n管理白名单 @XYBot 删除\n\n重置签到冷却:\n指令: 重置签到状态\n无参数\n\n获取机器人通讯录功能:\n指令: 获取机器人通讯录\n无参数\n\n查看已加载插件列表:\n指令:管理插件 列表\n\n热加载/卸载/重载插件\n指令:管理插件 操作名 插件名\n例如:管理插件 重载 lucky_draw\n\n如需批量管理插件 可以用 * 代替插件名称\n* 表示了所有插件(受保护的manage_plugins除外)" 39 | } 40 | -------------------------------------------------------------------------------- /plugins/command/news.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import aiohttp 8 | import yaml 9 | from bs4 import BeautifulSoup as bs 10 | from loguru import logger 11 | from wcferry import client 12 | 13 | from utils.plugin_interface import PluginInterface 14 | from wcferry_helper import XYBotWxMsg 15 | 16 | 17 | class news(PluginInterface): 18 | def __init__(self): 19 | config_path = "plugins/command/news.yml" 20 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 21 | config = yaml.safe_load(f.read()) 22 | 23 | self.important_news_count = config["important_news_count"] # 要获取的要闻数量 24 | 25 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 26 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 27 | 28 | try: 29 | url = "https://news.china.com/#" 30 | conn_ssl = aiohttp.TCPConnector(ssl=False) 31 | async with aiohttp.request('GET', url, connector=conn_ssl) as resp: 32 | news_html = await resp.text() 33 | await conn_ssl.close() 34 | 35 | soup = bs(news_html, "html.parser") 36 | 37 | focus_news = await self.get_focus_news(soup) 38 | focus_news_string = "" 39 | for title, link in focus_news: 40 | focus_news_string += f"📢{title}\n🔗{link}\n\n" 41 | 42 | important_news = await self.get_important_news(soup, self.important_news_count) 43 | important_news_string = "" 44 | for title, link, time in important_news: 45 | important_news_string += f"📰{title}\n🔗{link}\n🕒{time}\n\n" 46 | 47 | compose_message = f"----📰XYBot新闻📰----\n‼️‼️最新要闻‼️‼️\n{focus_news_string}\n⭐️⭐️要闻⭐️⭐️\n{important_news_string}" 48 | 49 | bot.send_text(compose_message, recv.roomid) 50 | logger.info(f'[发送信息]{compose_message}| [发送到] {recv.roomid}') 51 | 52 | except Exception as error: 53 | out_message = f'获取新闻失败!⚠️\n{error}' 54 | bot.send_text(out_message, recv.roomid) 55 | logger.error(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 56 | 57 | @staticmethod 58 | async def get_focus_news(soup) -> list: # 聚焦 59 | focus_news = [] 60 | focus_soup = soup.html.body.select('.focus_side > h3 > a') 61 | 62 | for new in focus_soup: 63 | focus_news.append([new.get_text(), new.get('href')]) 64 | 65 | return focus_news 66 | 67 | @staticmethod 68 | async def get_important_news(soup, count) -> list: # 要闻 69 | important_news = [] 70 | important_news_soup = soup.html.body.select('ul.item_list > li', limit=count) 71 | 72 | for new in important_news_soup: 73 | important_news.append([new.h3.a.get_text(), new.h3.a.get('href'), new.span.get_text(separator=' ')]) 74 | 75 | return important_news 76 | -------------------------------------------------------------------------------- /plugins/command/news.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "新闻", "news", "new", "获取新闻" ] 2 | plugin_name: "news" 3 | 4 | important_news_count: 5 -------------------------------------------------------------------------------- /plugins/command/points_leaderboard.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.database import BotDatabase 12 | from utils.plugin_interface import PluginInterface 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class points_leaderboard(PluginInterface): 17 | def __init__(self): 18 | config_path = "plugins/command/points_leaderboard.yml" 19 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 20 | config = yaml.safe_load(f.read()) 21 | 22 | self.leaderboard_top_number = config[ 23 | "leaderboard_top_number" 24 | ] # 显示积分榜前x名人 25 | 26 | self.db = BotDatabase() # 实例化数据库类 27 | 28 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 29 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 30 | 31 | data = self.db.get_highest_points(self.leaderboard_top_number) # 从数据库获取前x名积分数 32 | out_message = "-----XYBot积分排行榜-----" # 创建积分 33 | rank = 1 34 | for i in data: # 从数据库获取的数据中for循环 35 | nickname = self.db.get_nickname(i["WXID"]) # 获取昵称 36 | if not nickname: 37 | nickname = i["WXID"] 38 | 39 | out_message += f"\n{rank}. {nickname} {i["POINTS"]}分 👍" 40 | rank += 1 41 | # 组建积分榜信息 42 | 43 | out_message += "\n\n现在无法直接获取到昵称,需要发过消息的用户才能获取到昵称\n如果没发过只能显示wxid了" 44 | 45 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 46 | bot.send_text(out_message, recv.roomid) # 发送 47 | -------------------------------------------------------------------------------- /plugins/command/points_leaderboard.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "积分榜","积分榜单","查询积分榜","排行榜" ] 2 | plugin_name: "points_leaderboard" 3 | 4 | leaderboard_top_number: 30 -------------------------------------------------------------------------------- /plugins/command/points_trade.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.database import BotDatabase 12 | from utils.plugin_interface import PluginInterface 13 | from wcferry_helper import XYBotWxMsg 14 | 15 | 16 | class points_trade(PluginInterface): 17 | def __init__(self): 18 | config_path = "plugins/command/points_trade.yml" 19 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 20 | config = yaml.safe_load(f.read()) 21 | 22 | self.command_format_menu = config["command_format_menu"] # 指令格式 23 | 24 | self.max_points = config["max_points"] # 最多转帐多少积分 25 | self.min_points = config["min_points"] # 最少转帐多少积分 26 | 27 | self.db = BotDatabase() # 实例化机器人数据库类 28 | 29 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 30 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 31 | 32 | if recv.from_group() and len(recv.content) >= 3 and recv.content[1].isdigit(): # 判断是否为转账指令 33 | roomid, trader_wxid = recv.roomid, recv.sender # 获取群号和转账人wxid 34 | 35 | target_wxid = recv.ats[0] # 获取转账目标wxid 36 | 37 | points_num = recv.content[1] # 获取转账积分数 38 | 39 | error_message = self.get_error_message( 40 | target_wxid, trader_wxid, points_num 41 | ) # 获取是否有错误信息 42 | 43 | if not error_message: # 判断是否有错误信息和是否转账成功 44 | points_num = int(points_num) 45 | self.db.safe_trade_points(trader_wxid, target_wxid, points_num) 46 | # 记录日志和发送成功信息 47 | await self.log_and_send_success_message(bot, roomid, trader_wxid, target_wxid, points_num) 48 | bot.send_pat_msg(roomid, target_wxid) 49 | else: 50 | await self.log_and_send_error_message(bot, roomid, trader_wxid, error_message) # 记录日志和发送错误信息 51 | else: 52 | out_message = f"@{self.db.get_nickname(recv.sender)}\n-----XYBot-----\n转帐失败❌\n指令格式错误/在私聊转帐积分(仅可在群聊中转帐积分)❌\n\n{self.command_format_menu}" 53 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 54 | bot.send_text(out_message, recv.roomid, recv.sender) 55 | 56 | def get_error_message(self, target_wxid, trader_wxid, points_num: str): # 获取错误信息 57 | if not target_wxid: 58 | return "\n-----XYBot-----\n转帐失败❌\n转帐人不存在" 59 | elif not points_num.isdigit(): 60 | return "\n-----XYBot-----\n转帐失败❌\n转帐积分无效(必须为正整数!)" 61 | points_num = int(points_num) 62 | if not self.min_points <= points_num <= self.max_points: 63 | return f"\n-----XYBot-----\n转帐失败❌\n转帐积分无效(最大{self.max_points} 最小{self.min_points})" 64 | elif self.db.get_points(trader_wxid) < points_num: 65 | return f"\n-----XYBot-----\n积分不足!❌\n需要{points_num}点!" 66 | 67 | # 记录日志和发送成功信息 68 | async def log_and_send_success_message(self, bot: client.Wcf, roomid, trader_wxid, target_wxid, points_num): 69 | trader_nick = bot.get_alias_in_chatroom(trader_wxid, roomid) # 获取转账人昵称 70 | target_nick = bot.get_alias_in_chatroom(target_wxid, roomid) # 获取转账目标昵称 71 | 72 | logger.success( 73 | f"[积分转帐]转帐人:{trader_wxid} {trader_nick}|目标:{target_wxid} {target_nick}|群:{roomid}|积分数:{points_num}" 74 | ) 75 | trader_points, target_points = self.db.get_points(trader_wxid), self.db.get_points(target_wxid) 76 | out_message = f"@{trader_nick} @{target_nick}\n-----XYBot-----\n转帐成功✅! 你现在有{trader_points}点积分 {target_nick}现在有{target_points}点积分" 77 | logger.info(f'[发送@信息]{out_message}| [发送到] {roomid}') 78 | bot.send_text(out_message, roomid, ",".join([trader_wxid, target_wxid])) 79 | 80 | async def log_and_send_error_message(self, bot: client.Wcf, roomid, trader_wxid, error_message): # 记录日志和发送错误信息 81 | error_message = f"@{self.db.get_nickname(trader_wxid)}\n{error_message}" 82 | logger.info(f'[发送@信息]{error_message}| [发送到] {roomid}') 83 | bot.send_text(error_message, roomid, trader_wxid) 84 | -------------------------------------------------------------------------------- /plugins/command/points_trade.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "积分转帐","积分转账","转帐","转账","转账积分","转帐积分", "送积分", "转送积分", "积分转送" ] 2 | plugin_name: "points_trade" 3 | 4 | command_format_menu: "⚙️积分转账:\n积分转账 @xxx 数量" 5 | 6 | max_points: 100 7 | min_points: 1 -------------------------------------------------------------------------------- /plugins/command/query_points.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | from loguru import logger 8 | from wcferry import client 9 | 10 | from utils.database import BotDatabase 11 | from utils.plugin_interface import PluginInterface 12 | from wcferry_helper import XYBotWxMsg 13 | 14 | 15 | class query_points(PluginInterface): 16 | def __init__(self): 17 | self.db = BotDatabase() # 实例化机器人数据库类 18 | 19 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 20 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 21 | 22 | query_wxid = recv.sender # 获取查询wxid 23 | 24 | points_count = self.db.get_points(query_wxid) 25 | 26 | out_message = f"@{self.db.get_nickname(query_wxid)}\n-----XYBot-----\n你有{points_count}点积分!👍" # 从数据库获取积分数并创建信息 27 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 28 | bot.send_text(out_message, recv.roomid, query_wxid) 29 | -------------------------------------------------------------------------------- /plugins/command/query_points.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "查询积分","我的积分","积分查询" ] 2 | plugin_name: "query_points" -------------------------------------------------------------------------------- /plugins/command/random_picture.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import os 6 | import re 7 | import time 8 | 9 | import aiohttp 10 | import yaml 11 | from loguru import logger 12 | from wcferry import client 13 | 14 | from utils.plugin_interface import PluginInterface 15 | from wcferry_helper import XYBotWxMsg 16 | 17 | 18 | class random_picture(PluginInterface): 19 | def __init__(self): 20 | config_path = "plugins/command/random_picture.yml" 21 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 22 | config = yaml.safe_load(f.read()) 23 | 24 | self.random_picture_url = config["random_picture_url"] # 随机图片api 25 | 26 | self.cache_path = "resources/cache" 27 | 28 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 29 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 30 | 31 | # 图片缓存路径 32 | cache_path_original = os.path.abspath(os.path.join(self.cache_path, f"picture_{time.time_ns()}")) 33 | 34 | try: 35 | conn_ssl = aiohttp.TCPConnector(ssl=False) 36 | async with aiohttp.request("GET", url=self.random_picture_url, connector=conn_ssl) as req: 37 | cache_path = cache_path_original + "." + str(req.url).split('.')[-1] # 图片后缀 38 | with open(cache_path, "wb") as file: # 下载并保存 39 | file.write(await req.read()) 40 | file.close() 41 | 42 | await conn_ssl.close() 43 | 44 | logger.info(f'[发送信息](随机图图图片) {cache_path}| [发送到] {recv.roomid}') 45 | bot.send_image(os.path.abspath(cache_path), recv.roomid) # 发送图片 46 | bot.send_pat_msg(recv.roomid, recv.sender) # 发送拍一拍消息 47 | 48 | except Exception as error: 49 | out_message = f"-----XYBot-----\n出现错误❌!{error}" 50 | logger.error(error) 51 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 52 | bot.send_text(out_message, recv.roomid) # 发送 53 | -------------------------------------------------------------------------------- /plugins/command/random_picture.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "随机图图" ] 2 | plugin_name: "random_picture" 3 | 4 | random_picture_url: "https://api.horosama.com/random.php" -------------------------------------------------------------------------------- /plugins/command/red_packet.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import os 6 | import random 7 | import re 8 | import time 9 | 10 | import yaml 11 | from captcha.image import ImageCaptcha 12 | from loguru import logger 13 | from wcferry import client 14 | 15 | from utils.database import BotDatabase 16 | from utils.plugin_interface import PluginInterface 17 | from wcferry_helper import XYBotWxMsg 18 | 19 | 20 | class red_packet(PluginInterface): 21 | def __init__(self): 22 | config_path = "plugins/command/red_packet.yml" 23 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 24 | config = yaml.safe_load(f.read()) 25 | 26 | self.command_format_menu = config["command_format_menu"] # 指令格式 27 | 28 | self.max_point = config["max_point"] # 最大积分 29 | self.min_point = config["min_point"] # 最小积分 30 | self.max_packet = config["max_packet"] # 最大红包数量 31 | self.max_time = config["max_time"] # 红包超时时间 32 | 33 | main_config_path = "main_config.yml" 34 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 35 | main_config = yaml.safe_load(f.read()) 36 | 37 | self.command_prefix = main_config["command_prefix"] 38 | 39 | self.db = BotDatabase() # 实例化机器人数据库类 40 | 41 | cache_path = "resources/cache" # 检测是否有cache文件夹 42 | if not os.path.exists(cache_path): 43 | logger.info("检测到未创建cache缓存文件夹") 44 | os.makedirs(cache_path) 45 | logger.info("已创建cache文件夹") 46 | 47 | self.red_packets = {} # 红包列表 48 | 49 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 50 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 51 | 52 | if len(recv.content) == 3: # 判断是否为红包指令 53 | await self.send_red_packet(bot, recv) 54 | elif len(recv.content) == 2: # 判断是否为抢红包指令 55 | await self.grab_red_packet(bot, recv) 56 | else: # 指令格式错误 57 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n❌命令格式错误!{self.command_format_menu}") 58 | 59 | async def send_red_packet(self, bot: client.Wcf, recv: XYBotWxMsg): 60 | red_packet_sender = recv.sender 61 | 62 | # 判断是否有错误 63 | error = "" 64 | if not recv.from_group(): 65 | error = "-----XYBot-----\n❌红包只能在群里发!" 66 | elif not recv.content[1].isdigit() or not recv.content[2].isdigit(): 67 | error = "-----XYBot-----\n❌指令格式错误!请查看菜单!" 68 | elif int(recv.content[1]) > self.max_point or int(recv.content[1]) < self.min_point: 69 | error = f"-----XYBot-----\n⚠️积分无效!最大{self.max_point},最小{self.min_point}!" 70 | elif int(recv.content[2]) > self.max_packet: 71 | error = f"-----XYBot-----\n⚠️红包数量无效!最大{self.max_packet}!" 72 | elif int(recv.content[2]) > int(recv.content[1]): 73 | error = "-----XYBot-----\n❌红包数量不能大于红包积分!" 74 | 75 | # 判断是否有足够积分 76 | if not error: 77 | if self.db.get_points(red_packet_sender) < int(recv.content[1]): 78 | error = "-----XYBot-----\n❌积分不足!" 79 | 80 | if not error: 81 | red_packet_points = int(recv.content[1]) # 红包积分 82 | red_packet_amount = int(recv.content[2]) # 红包数量 83 | red_packet_chatroom = recv.roomid # 红包所在群聊 84 | 85 | red_packet_sender_nick = self.db.get_nickname(red_packet_sender) # 获取昵称 86 | if not red_packet_sender_nick: 87 | red_packet_sender_nick = red_packet_sender 88 | 89 | red_packet_points_list = self.split_integer( 90 | red_packet_points, red_packet_amount 91 | ) # 随机分红包积分 92 | 93 | chr_5, captcha_path = self.generate_captcha() # 生成口令 94 | captcha_path = os.path.abspath(captcha_path) # 获取口令路径 95 | 96 | new_red_packet = { 97 | "points": red_packet_points, 98 | "amount": red_packet_amount, 99 | "sender": red_packet_sender, 100 | "list": red_packet_points_list, 101 | "grabbed": [], 102 | "time": time.time(), 103 | "chatroom": red_packet_chatroom, 104 | "sender_nick": red_packet_sender_nick, 105 | } # 红包信息 106 | 107 | self.red_packets[chr_5] = new_red_packet # 把红包放入红包列表 108 | self.db.add_points(red_packet_sender, red_packet_points * -1) # 扣除积分 109 | 110 | # 组建信息 111 | out_message = f"-----XYBot-----\n{red_packet_sender_nick} 发送了一个红包!\n\n🧧红包金额:{red_packet_points}点积分\n🧧红包数量:{red_packet_amount}个\n\n🧧红包口令请见下图!\n\n快输入指令来抢红包!\n指令:{self.command_prefix}抢红包 口令" 112 | 113 | # 发送信息 114 | bot.send_text(out_message, recv.roomid) 115 | logger.info(f'[发送信息] (红包口令图片) {captcha_path} | [发送到] {recv.roomid}') 116 | 117 | bot.send_image(captcha_path, recv.roomid) 118 | 119 | 120 | else: 121 | await self.send_friend_or_group(bot, recv, error) # 发送错误信息 122 | 123 | async def grab_red_packet(self, bot: client.Wcf, recv: XYBotWxMsg): 124 | red_packet_grabber = recv.sender 125 | 126 | req_captcha = recv.content[1] 127 | 128 | # 判断是否有错误 129 | error = "" 130 | if req_captcha not in self.red_packets.keys(): 131 | error = "-----XYBot-----\n❌口令错误或无效!" 132 | elif not self.red_packets[req_captcha]["list"]: 133 | error = "-----XYBot-----\n⚠️红包已被抢完!" 134 | elif not recv.from_group(): 135 | error = "-----XYBot-----\n❌红包只能在群里抢!" 136 | elif red_packet_grabber in self.red_packets[req_captcha]["grabbed"]: 137 | error = "-----XYBot-----\n⚠️你已经抢过这个红包了!" 138 | elif self.red_packets[req_captcha]["sender"] == red_packet_grabber: 139 | error = "-----XYBot-----\n❌不能抢自己的红包!" 140 | 141 | if not error: 142 | try: # 抢红包 143 | grabbed_points = self.red_packets[req_captcha][ 144 | "list" 145 | ].pop() # 抢到的积分 146 | self.red_packets[req_captcha]["grabbed"].append( 147 | red_packet_grabber 148 | ) # 把抢红包的人加入已抢列表 149 | 150 | red_packet_grabber_nick = self.db.get_nickname(red_packet_grabber) # 获取昵称 151 | if not red_packet_grabber_nick: 152 | red_packet_grabber_nick = red_packet_grabber 153 | 154 | self.db.add_points(red_packet_grabber, grabbed_points) # 增加积分 155 | 156 | # 组建信息 157 | out_message = f"-----XYBot-----\n🧧恭喜 {red_packet_grabber_nick} 抢到了 {grabbed_points} 点积分!" 158 | await self.send_friend_or_group(bot, recv, out_message) 159 | bot.send_pat_msg(recv.roomid, red_packet_grabber) # 发送拍一拍消息 160 | 161 | # 判断是否抢完 162 | if not self.red_packets[req_captcha]["list"]: 163 | self.red_packets.pop(req_captcha) 164 | 165 | except IndexError: 166 | error = "-----XYBot-----\n❌红包已被抢完!" 167 | await self.send_friend_or_group(bot, recv, error) 168 | 169 | return 170 | 171 | else: 172 | # 发送错误信息 173 | await self.send_friend_or_group(bot, recv, error) 174 | 175 | return 176 | 177 | @staticmethod 178 | def generate_captcha(): # 生成口令 179 | chr_all = [ 180 | "a", 181 | "b", 182 | "d", 183 | "f", 184 | "g", 185 | "h", 186 | "k", 187 | "m", 188 | "n", 189 | "p", 190 | "q", 191 | "t", 192 | "w", 193 | "x", 194 | "y", 195 | "2", 196 | "3", 197 | "4", 198 | "6", 199 | "7", 200 | "8", 201 | "9", 202 | ] 203 | chr_5 = "".join(random.sample(chr_all, 5)) 204 | captcha_image = ImageCaptcha().generate_image(chr_5) 205 | path = f"resources/cache/{chr_5}.jpg" 206 | captcha_image.save(path) 207 | 208 | return chr_5, path 209 | 210 | @staticmethod 211 | def split_integer(num, count): 212 | # 初始化每个数为1 213 | result = [1] * count 214 | remaining = num - count 215 | 216 | # 随机分配剩余的部分 217 | while remaining > 0: 218 | index = random.randint(0, count - 1) 219 | result[index] += 1 220 | remaining -= 1 221 | 222 | return result 223 | 224 | async def expired_red_packets_check(self, bot: client.Wcf): # 检查是否有超时红包 225 | logger.info("[计划任务]检查是否有超时的红包") 226 | for key in list(self.red_packets.keys()): 227 | if time.time() - self.red_packets[key]["time"] > self.max_time: # 判断是否超时 228 | 229 | red_packet_sender = self.red_packets[key].sender # 获取红包发送人 230 | red_packet_points_left_sum = sum(self.red_packets[key]["list"]) # 获取剩余积分 231 | red_packet_chatroom = self.red_packets[key]["chatroom"] # 获取红包所在群聊 232 | red_packet_sender_nick = self.red_packets[key]["sender_nick"] # 获取红包发送人昵称 233 | 234 | self.db.add_points(red_packet_sender, red_packet_points_left_sum) # 归还积分 235 | self.red_packets.pop(key) # 删除红包 236 | logger.info("[红包]有红包超时,已归还积分!") # 记录日志 237 | 238 | # 组建信息并发送 239 | out_message = f"-----XYBot-----\n🧧发现有红包 {key} 超时!已归还剩余 {red_packet_points_left_sum} 积分给 {red_packet_sender_nick}" 240 | bot.send_text(out_message, red_packet_chatroom) 241 | logger.info(f"[发送信息]{out_message}| [发送到] {red_packet_chatroom}") 242 | 243 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 244 | if recv.from_group(): # 判断是群还是私聊 245 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 246 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 247 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 248 | else: 249 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 250 | bot.send_text(out_message, recv.roomid) # 发送 251 | -------------------------------------------------------------------------------- /plugins/command/red_packet.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "发红包", "抢红包" ] 2 | plugin_name: "red_packet" 3 | 4 | command_format_menu: "⚙️发红包:\n发红包 数量 金额\n⚙️抢红包:\n抢红包 红包验证码" 5 | 6 | max_point: 5000 7 | min_point: 10 8 | 9 | max_packet: 100 10 | max_time: 43200 -------------------------------------------------------------------------------- /plugins/command/sign_in.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import random 6 | import re 7 | from datetime import datetime 8 | from datetime import timedelta 9 | 10 | import pytz 11 | import yaml 12 | from loguru import logger 13 | from wcferry import client 14 | 15 | from utils.database import BotDatabase 16 | from utils.plugin_interface import PluginInterface 17 | from wcferry_helper import XYBotWxMsg 18 | 19 | 20 | class sign_in(PluginInterface): 21 | def __init__(self): 22 | config_path = "plugins/command/sign_in.yml" 23 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 24 | config = yaml.safe_load(f.read()) 25 | 26 | self.min_points = config["min_points"] # 最小积分 27 | self.max_points = config["max_points"] # 最大积分 28 | 29 | self.min_lucky_star = config["min_lucky_star"] # 最小幸运星数 30 | self.max_lucky_star = config["max_lucky_star"] # 最大幸运星数 31 | self.lucky_star_message = config["lucky_star_message"] 32 | 33 | main_config_path = "main_config.yml" 34 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 35 | main_config = yaml.safe_load(f.read()) 36 | 37 | self.timezone = main_config["timezone"] # 时区 38 | 39 | self.db = BotDatabase() 40 | 41 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 42 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 43 | 44 | signin_points = random.randint(self.min_points, self.max_points) # 随机3-20积分 45 | 46 | sign_wxid = recv.sender 47 | 48 | signstat = str(self.db.get_stat(sign_wxid)) # 从数据库获取签到状态 49 | 50 | if self.signstat_check(signstat): # 如果今天未签到 51 | self.db.add_points(sign_wxid, signin_points) # 在数据库加积分 52 | now_datetime = datetime.now(tz=pytz.timezone(self.timezone)).strftime("%Y%m%d") # 获取现在格式化后时间 53 | self.db.set_stat(sign_wxid, now_datetime) # 设置签到状态为现在格式化后时间 54 | 55 | # 运势 56 | lucky_num = random.randint(self.min_lucky_star, self.max_lucky_star) 57 | lucky_star = "⭐️" * lucky_num 58 | lucky_star_message = f"你的运势:{lucky_star}\n{self.lucky_star_message.get(lucky_num)}" 59 | 60 | out_message = f"@{self.db.get_nickname(sign_wxid)}\n-----XYBot-----\n签到成功!你领到了{signin_points}个积分!✅\n\n{lucky_star_message}" # 创建发送信息 61 | logger.info(f"[发送@信息]{out_message}| [发送到] {recv.roomid}") 62 | bot.send_text(out_message, recv.roomid, sign_wxid) 63 | 64 | else: # 今天已签到,不加积分 65 | next_sign_in_date = datetime.strptime(signstat, "%Y%m%d") + timedelta(days=1) 66 | next_sign_in_date_formatted = next_sign_in_date.strftime("%Y年%m月%d日") 67 | out_message = f"@{self.db.get_nickname(sign_wxid)}\n-----XYBot-----\n❌你今天已经签到过了,每日凌晨刷新签到哦!下一次签到日期:{next_sign_in_date_formatted}" # 创建信息 68 | logger.info(f"[发送@信息]{out_message}| [发送到] {recv.roomid}") 69 | bot.send_text(out_message, recv.roomid, sign_wxid) 70 | 71 | bot.send_pat_msg(recv.roomid, sign_wxid) 72 | logger.info(f"[发送拍一拍] 拍了拍: {sign_wxid} | 发送到: {recv.roomid}") 73 | 74 | def signstat_check(self, signstat): # 检查签到状态 75 | signstat = "20000101" if signstat in ["0", "1"] else signstat 76 | last_sign_date = datetime.strptime(signstat, "%Y%m%d").date() 77 | now_date = datetime.now(tz=pytz.timezone(self.timezone)).date() 78 | return (now_date - last_sign_date).days >= 1 79 | -------------------------------------------------------------------------------- /plugins/command/sign_in.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "签到","qd" ] 2 | plugin_name: "sign_in" 3 | 4 | min_points: 3 5 | max_points: 20 6 | 7 | min_lucky_star: 1 8 | max_lucky_star: 5 9 | 10 | lucky_star_message: { 11 | 1: "你今天的运势不太好哦,不过还是请加油!ಠ_ಠ", 12 | 2: "你今天的运势一般般,继续努力哦!(´ο`*)", 13 | 3: "你今天的运势还不错,加油哦!(^v^)", 14 | 4: "你今天的运势很好,保持下去哦!^ω^", 15 | 5: "你今天的运势超级好!!(≧▽≦)" 16 | } -------------------------------------------------------------------------------- /plugins/command/warthunder.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | # 5 | # This program is licensed under the GNU General Public License v3.0. 6 | 7 | import re 8 | 9 | import aiohttp 10 | import yaml 11 | from loguru import logger 12 | from wcferry import client 13 | 14 | from utils.database import BotDatabase 15 | from utils.plugin_interface import PluginInterface 16 | from wcferry_helper import XYBotWxMsg 17 | 18 | 19 | class warthunder(PluginInterface): 20 | def __init__(self): 21 | config_path = "plugins/command/warthunder.yml" 22 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 23 | config = yaml.safe_load(f.read()) 24 | 25 | self.command_format_menu = config["command_format_menu"] # 指令格式 26 | 27 | self.warthunder_player_api_url = config["warthunder_player_api_url"] # 要获取的要闻数量 28 | 29 | self.db = BotDatabase() 30 | 31 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 32 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 33 | 34 | error = "" 35 | if len(recv.content) < 2: 36 | error = f"-----XYBot-----\n参数错误!❌\n\n{self.command_format_menu}" 37 | 38 | if error: 39 | await self.send_friend_or_group(bot, recv, error) 40 | return 41 | 42 | player_name = ' '.join(recv.content[1:]) 43 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n正在查询玩家{player_name}的数据,请稍等...😄") 44 | 45 | data = await self.get_player_data(player_name) 46 | if isinstance(data, Exception): 47 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n查询失败,错误信息:{data}") 48 | return 49 | elif data.get("error", False): 50 | await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n目前API使用人数过多,请等候1分钟后再使用。🙏") 51 | return 52 | elif data.get("code", 200) == 404: 53 | await self.send_friend_or_group(bot, recv, 54 | f"-----XYBot-----\n未找到玩家{player_name}的数据,请检查昵称是否正确。🤔") 55 | return 56 | else: 57 | out_message = await self.parse_player_data(data) 58 | await self.send_friend_or_group(bot, recv, out_message) 59 | 60 | async def get_player_data(self, player_name): 61 | try: 62 | async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(60)) as session: 63 | async with session.get(f"{self.warthunder_player_api_url}{player_name}") as resp: 64 | data = await resp.json() 65 | return data 66 | except Exception as e: 67 | return e 68 | 69 | @staticmethod 70 | async def parse_player_data(data): 71 | nickname = data.get("nickname") 72 | clan_name = data.get("clan_name") 73 | player_level = data.get("player_level") 74 | register_date = data.get("register_date") 75 | general_info = f"{clan_name} {nickname}\n等级:{player_level}级\n入坑战雷日期:{register_date}" 76 | 77 | realistic_data = data.get("statistics").get("realistic") 78 | realistic_battle_missions = realistic_data.get("CompletedMissions") 79 | realistic_winrate = realistic_data.get("VictoriesPerBattlesRatio") 80 | realistic_deaths = realistic_data.get("Deaths") 81 | realistic_sl_earned = realistic_data.get("LionsEarned") 82 | realistic_play_time = realistic_data.get("PlayTime") 83 | realistic_target_destroyed = realistic_data.get("AirTargetsDestroyed") + realistic_data.get( 84 | "GroundTargetsDestroyed") + realistic_data.get("NavalTargetsDestroyed") 85 | realistic_info = f"【历史性能】\n⚔️参与次数:{realistic_battle_missions}\n✌️胜率:{realistic_winrate}\n💀死亡数:{realistic_deaths}\n💥总击毁目标:{realistic_target_destroyed}\n🪙获得银狮:{realistic_sl_earned}\n⌛️游戏时间:{realistic_play_time}" 86 | 87 | aviation_rb_data = realistic_data.get("aviation") 88 | aviation_rb_battles = aviation_rb_data.get("AirBattle") 89 | aviation_rb_target_destroyed = aviation_rb_data.get("TotalTargetsDestroyed") 90 | aviation_tb_air_targets_destroyed = aviation_rb_data.get("AirTargetsDestroyed") 91 | aviation_rb_play_time = aviation_rb_data.get("TimePlayedInAirBattles") 92 | aviation_rb_info = f"【空战-历史性能】\n⚔️参与次数:{aviation_rb_battles}\n💥总击毁目标:{aviation_rb_target_destroyed}\n💥击毁空中目标:{aviation_tb_air_targets_destroyed}\n⌛️游戏时间:{aviation_rb_play_time}" 93 | 94 | ground_rb_data = realistic_data.get("ground") 95 | ground_rb_battles = ground_rb_data.get("GroundBattles") 96 | ground_rb_target_destroyed = ground_rb_data.get("TotalTargetsDestroyed") 97 | ground_rb_ground_targets_destroyed = ground_rb_data.get("GroundTargetsDestroyed") 98 | ground_rb_play_time = ground_rb_data.get("TimePlayedInGroundBattles") 99 | ground_rb_info = f"【陆战-历史性能】\n⚔️参与次数:{ground_rb_battles}\n💥总击毁目标:{ground_rb_target_destroyed}\n💥击毁地面目标:{ground_rb_ground_targets_destroyed}\n⌛️游戏时间:{ground_rb_play_time}" 100 | 101 | out_message = f"-----XYBot-----\n{general_info}\n\n{realistic_info}\n\n{aviation_rb_info}\n\n{ground_rb_info}" 102 | return out_message 103 | 104 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 105 | if recv.from_group(): # 判断是群还是私聊 106 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 107 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 108 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 109 | else: 110 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 111 | bot.send_text(out_message, recv.roomid) # 发送 112 | -------------------------------------------------------------------------------- /plugins/command/warthunder.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "战雷数据", "战雷查询" ] 2 | plugin_name: "warthunder" 3 | 4 | command_format_menu: "⚙️查询战雷玩家数据:战雷查询 玩家昵称" 5 | 6 | warthunder_player_api_url: "https://wtapi.yangres.com/player/" -------------------------------------------------------------------------------- /plugins/command/weather.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import aiohttp 8 | import yaml 9 | from loguru import logger 10 | from wcferry import client 11 | 12 | from utils.database import BotDatabase 13 | from utils.plugin_interface import PluginInterface 14 | from wcferry_helper import XYBotWxMsg 15 | 16 | 17 | class weather(PluginInterface): 18 | def __init__(self): 19 | config_path = "plugins/command/weather.yml" 20 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 21 | config = yaml.safe_load(f.read()) 22 | 23 | self.command_format_menu = config["command_format_menu"] # 指令格式 24 | 25 | self.weather_api_key = config["weather_api_key"] 26 | 27 | self.db = BotDatabase() 28 | 29 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 30 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 31 | 32 | error = '' 33 | if len(recv.content) != 2: 34 | error = f'指令格式错误!\n\n{self.command_format_menu}' 35 | 36 | if not error: 37 | # 首先请求geoapi,查询城市的id 38 | request_city = recv.content[1] 39 | geo_api_url = f'https://geoapi.qweather.com/v2/city/lookup?key={self.weather_api_key}&number=1&location={request_city}' 40 | 41 | conn_ssl = aiohttp.TCPConnector(verify_ssl=False) 42 | async with aiohttp.request('GET', url=geo_api_url, connector=conn_ssl) as response: 43 | geoapi_json = await response.json() 44 | await conn_ssl.close() 45 | 46 | if geoapi_json['code'] == '200': # 如果城市存在 47 | request_city_id = geoapi_json['location'][0]['id'] 48 | request_city_name = geoapi_json['location'][0]['name'] 49 | 50 | # 请求现在天气api 51 | conn_ssl = aiohttp.TCPConnector(verify_ssl=False) 52 | now_weather_api_url = f'https://devapi.qweather.com/v7/weather/now?key={self.weather_api_key}&location={request_city_id}' 53 | async with aiohttp.request('GET', url=now_weather_api_url, connector=conn_ssl) as response: 54 | now_weather_api_json = await response.json() 55 | await conn_ssl.close() 56 | 57 | # 请求预报天气api 58 | conn_ssl = aiohttp.TCPConnector(verify_ssl=False) 59 | weather_forecast_api_url = f'https://devapi.qweather.com/v7/weather/7d?key={self.weather_api_key}&location={request_city_id}' 60 | async with aiohttp.request('GET', url=weather_forecast_api_url, connector=conn_ssl) as response: 61 | weather_forecast_api_json = await response.json() 62 | await conn_ssl.close() 63 | 64 | out_message = self.compose_weather_message(request_city_name, now_weather_api_json, 65 | weather_forecast_api_json) 66 | await self.send_friend_or_group(bot, recv, out_message) 67 | 68 | elif geoapi_json['code'] == '404': 69 | error = '-----XYBot-----\n⚠️城市不存在!' 70 | await self.send_friend_or_group(bot, recv, error) 71 | else: 72 | error = f'-----XYBot-----\n⚠️请求失败!\n{geoapi_json}' 73 | await self.send_friend_or_group(bot, recv, error) 74 | 75 | 76 | else: 77 | await self.send_friend_or_group(bot, recv, error) 78 | 79 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 80 | if recv.from_group(): # 判断是群还是私聊 81 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 82 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 83 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 84 | else: 85 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 86 | bot.send_text(out_message, recv.roomid) # 发送 87 | 88 | @staticmethod 89 | def compose_weather_message(city_name, now_weather_api_json, weather_forecast_api_json): 90 | update_time = now_weather_api_json['updateTime'] 91 | now_temperature = now_weather_api_json['now']['temp'] 92 | now_feelslike = now_weather_api_json['now']['feelsLike'] 93 | now_weather = now_weather_api_json['now']['text'] 94 | now_wind_direction = now_weather_api_json['now']['windDir'] 95 | now_wind_scale = now_weather_api_json['now']['windScale'] 96 | now_humidity = now_weather_api_json['now']['humidity'] 97 | now_precip = now_weather_api_json['now']['precip'] 98 | now_visibility = now_weather_api_json['now']['vis'] 99 | now_uvindex = weather_forecast_api_json['daily'][0]['uvIndex'] 100 | 101 | message = f'-----XYBot-----\n{city_name} 实时天气☁️\n更新时间:{update_time}⏰\n\n🌡️当前温度:{now_temperature}℃\n🌡️体感温度:{now_feelslike}℃\n☁️天气:{now_weather}\n☀️紫外线指数:{now_uvindex}\n🌬️风向:{now_wind_direction}\n🌬️风力:{now_wind_scale}级\n💦湿度:{now_humidity}%\n🌧️降水量:{now_precip}mm/h\n👀能见度:{now_visibility}km\n\n☁️未来3天 {city_name} 天气:\n' 102 | for day in weather_forecast_api_json['daily'][1:4]: 103 | date = '.'.join([i.lstrip('0') for i in day['fxDate'].split('-')[1:]]) 104 | weather = day['textDay'] 105 | max_temp = day['tempMax'] 106 | min_temp = day['tempMin'] 107 | uv_index = day['uvIndex'] 108 | message += f'{date} {weather} 最高🌡️{max_temp}℃ 最低🌡️{min_temp}℃ ☀️紫外线:{uv_index}\n' 109 | 110 | return message 111 | -------------------------------------------------------------------------------- /plugins/command/weather.yml: -------------------------------------------------------------------------------- 1 | keywords: [ "天气", "获取天气" ] 2 | plugin_name: "weather" 3 | 4 | command_format_menu: "⚙️获取天气:\n天气 城市名称" 5 | 6 | weather_api_key: "" 7 | 8 | # 老api改收费了 新的api需要申请,链接:https://dev.qweather.com/ 9 | # 项目订阅选免费订阅即可,把获得到的Key (不是Public ID 而是Private KEY) 填到上面引号中 10 | -------------------------------------------------------------------------------- /plugins/image/_image_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | from loguru import logger 6 | from wcferry import client 7 | 8 | from utils.plugin_interface import PluginInterface 9 | from wcferry_helper import XYBotWxMsg 10 | 11 | 12 | class image_test(PluginInterface): 13 | def __init__(self): 14 | pass 15 | 16 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 17 | logger.debug(f"收到图片消息!{recv}") 18 | 19 | bot.send_text(f"收到图片消息!{recv}", recv.roomid) 20 | bot.send_image(recv.image, recv.roomid) 21 | -------------------------------------------------------------------------------- /plugins/join_group/_join_group_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | from loguru import logger 6 | from wcferry import client 7 | 8 | from utils.plugin_interface import PluginInterface 9 | from wcferry_helper import XYBotWxMsg 10 | 11 | 12 | class join_group_test(PluginInterface): 13 | def __init__(self): 14 | pass 15 | 16 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 17 | logger.debug(f"收到入群消息!{recv}") 18 | bot.send_text(str(recv), recv.roomid) 19 | -------------------------------------------------------------------------------- /plugins/join_group/join_notification.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | import os 5 | import re 6 | 7 | from loguru import logger 8 | from wcferry import client 9 | 10 | from utils.plugin_interface import PluginInterface 11 | from wcferry_helper import XYBotWxMsg 12 | 13 | 14 | class join_notification(PluginInterface): 15 | def __init__(self): 16 | self.logo_path = os.path.abspath("resources/XYBotLogo.png") 17 | 18 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 19 | join_group_msg = recv.content 20 | 21 | # 邀请进来的 22 | if "邀请" in join_group_msg: 23 | # 通过正则表达式提取邀请者的名字 24 | invite_pattern = r'"([^"]+)"邀请"([^"]+)"加入了群聊' 25 | match = re.search(invite_pattern, join_group_msg) 26 | 27 | if match: 28 | joiner = match.group(2) 29 | await self.send_welcome(bot, recv.roomid, joiner) 30 | 31 | async def send_welcome(self, bot: client.Wcf, roomid: str, joiner: str): 32 | out_message = f"-------- XYBot ---------\n👏欢迎新成员 {joiner} 加入本群!⭐️\n⚙️输入 菜单 获取玩法哦😄" 33 | logger.info(f'[发送信息]{out_message}| [发送到] {roomid}') 34 | bot.send_text(out_message, roomid) 35 | -------------------------------------------------------------------------------- /plugins/mention/_mention_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | from loguru import logger 6 | from wcferry import client 7 | 8 | from utils.plugin_interface import PluginInterface 9 | from wcferry_helper import XYBotWxMsg 10 | 11 | 12 | class mention_test(PluginInterface): 13 | def __init__(self): 14 | pass 15 | 16 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 17 | logger.debug(f"收到@消息!{recv}") 18 | bot.send_text(str(recv), recv.roomid) 19 | -------------------------------------------------------------------------------- /plugins/mention/mention_gpt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import base64 6 | import json 7 | import os 8 | import time 9 | 10 | import yaml 11 | from loguru import logger 12 | from openai import AsyncOpenAI 13 | from wcferry import client 14 | 15 | from utils.database import BotDatabase 16 | from utils.plugin_interface import PluginInterface 17 | from wcferry_helper import XYBotWxMsg 18 | 19 | 20 | class mention_gpt(PluginInterface): 21 | def __init__(self): 22 | config_path = "plugins/mention/mention_gpt.yml" 23 | with open(config_path, "r", encoding="utf-8") as f: # 读取设置 24 | config = yaml.safe_load(f.read()) 25 | 26 | self.gpt_version = config["gpt_version"] # gpt版本 27 | self.gpt_point_price = config["gpt_point_price"] # gpt使用价格(单次) 28 | self.gpt_max_token = config["gpt_max_token"] # gpt 最大token 29 | self.gpt_temperature = config["gpt_temperature"] # gpt 温度 30 | 31 | self.model_name = config["model_name"] # 模型名称 32 | self.image_quality = config["image_quality"] # 图片质量 33 | self.image_size = config["image_size"] # 图片尺寸 34 | self.image_price = config["image_price"] # 图片价格 35 | 36 | self.max_possible_points = self.gpt_point_price * 2 + self.image_price # 消耗积分极限 37 | 38 | main_config_path = "main_config.yml" 39 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 40 | main_config = yaml.safe_load(f.read()) 41 | 42 | self.admins = main_config["admins"] # 获取管理员列表 43 | 44 | self.openai_api_base = main_config["openai_api_base"] # openai api 链接 45 | self.openai_api_key = main_config["openai_api_key"] # openai api 密钥 46 | 47 | sensitive_words_path = "sensitive_words.yml" # 加载敏感词yml 48 | with open(sensitive_words_path, "r", encoding="utf-8") as f: # 读取设置 49 | sensitive_words_config = yaml.safe_load(f.read()) 50 | self.sensitive_words = sensitive_words_config["sensitive_words"] # 敏感词列表 51 | 52 | self.db = BotDatabase() 53 | 54 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 55 | user_wxid = recv.sender 56 | gpt_request_message = recv.content 57 | 58 | error = "" 59 | if self.db.get_points( 60 | user_wxid) < self.max_possible_points and user_wxid not in self.admins and not self.db.get_whitelist( 61 | user_wxid): # 积分不够 62 | error = f"本功能可消耗最多 {self.max_possible_points} 点积分,您的积分不足,无法使用GPT功能!⚠️" 63 | elif not self.senstitive_word_check(gpt_request_message): # 有敏感词 64 | error = "您的问题中包含敏感词,请重新输入!⚠️" 65 | 66 | if error: 67 | await self.send_friend_or_group(bot, recv, error) 68 | return 69 | 70 | chat_completion = await self.chatgpt(gpt_request_message) 71 | 72 | if not chat_completion[0]: 73 | logger.error(str(chat_completion[1])) 74 | out_message = f"出现错误,请稍后再试!⚠️\n错误信息:\n{str(chat_completion[1])}" 75 | await self.send_friend_or_group(bot, recv, out_message) 76 | 77 | chat_completion = chat_completion[1] 78 | if chat_completion.choices[0].message.tool_calls: 79 | tool_call = chat_completion.choices[0].message.tool_calls[0] 80 | prompt = json.loads(tool_call.function.arguments).get("prompt") 81 | 82 | success = await self.generate_and_send_picture(prompt, bot, recv) 83 | 84 | function_call_result_message = { 85 | "role": "tool", 86 | "content": json.dumps({ 87 | "prompt": prompt, 88 | "success": success, 89 | }), 90 | "tool_call_id": tool_call.id 91 | } 92 | 93 | chat_completion_2 = await self.function_call_result_to_gpt(gpt_request_message, chat_completion, function_call_result_message) 94 | await self.send_friend_or_group(bot, recv, chat_completion_2.choices[0].message.content) 95 | 96 | if user_wxid not in self.admins and not self.db.get_whitelist(user_wxid): 97 | minus_points = self.gpt_point_price * 2 + self.image_price 98 | self.db.add_points(user_wxid, minus_points * -1) 99 | 100 | else: 101 | await self.send_friend_or_group(bot, recv, chat_completion.choices[0].message.content) 102 | if user_wxid not in self.admins and not self.db.get_whitelist(user_wxid): 103 | self.db.add_points(user_wxid, self.gpt_point_price * -1) 104 | 105 | async def chatgpt(self, gpt_request_message): 106 | client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) 107 | try: 108 | tools = [ 109 | { 110 | "type": "function", 111 | "function": { 112 | "name": "generate_and_send_picture", 113 | "description": "Generate an image using the user's description. Call this when the user requests an image, for example when a user asks 'Can you show me a picture of a cat?'. The function returns true on sucess.", 114 | "parameters": { 115 | "type": "object", 116 | "properties": { 117 | "prompt": { 118 | "type": "string", 119 | "description": "The prompt of the image to generate." 120 | }, 121 | 122 | }, 123 | "required": ["prompt"], 124 | "additionalProperties": False, 125 | } 126 | } 127 | } 128 | ] 129 | chat_completion = await client.chat.completions.create( 130 | messages=[ 131 | { 132 | "role": "system", 133 | "content": "You are a helpful, creative, clever, and very friendly assistant. You output in plain text instead of markdown.", 134 | }, 135 | { 136 | "role": "user", 137 | "content": gpt_request_message, 138 | } 139 | ], 140 | tools=tools, 141 | model=self.gpt_version, 142 | temperature=self.gpt_temperature, 143 | max_tokens=self.gpt_max_token, 144 | parallel_tool_calls=False 145 | ) 146 | return True, chat_completion 147 | except Exception as error: 148 | return False, error 149 | 150 | async def function_call_result_to_gpt(self, gpt_request_message, chat_completion, function_call_result_message): 151 | client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) 152 | message_payload = [ 153 | { 154 | "role": "system", 155 | "content": "You are a helpful, creative, clever, and very friendly assistant. You output in plain text instead of markdown.", 156 | }, 157 | { 158 | "role": "user", 159 | "content": gpt_request_message, 160 | }, 161 | chat_completion.choices[0].message.dict(), 162 | function_call_result_message 163 | ] 164 | response_chat_completion = await client.chat.completions.create( 165 | messages=message_payload, 166 | model=self.gpt_version, 167 | temperature=self.gpt_temperature, 168 | max_tokens=self.gpt_max_token, 169 | ) 170 | return response_chat_completion 171 | 172 | async def generate_and_send_picture(self, prompt: str, bot: client.Wcf, recv: XYBotWxMsg) -> bool: 173 | try: 174 | await self.send_friend_or_group(bot, recv, f"⚙️生成图片中...") 175 | save_path = await self.dalle3(prompt) 176 | bot.send_image(save_path, recv.roomid) 177 | logger.info(f"发送图片: {save_path}") 178 | return True 179 | except Exception as error: 180 | logger.error(f"Error: {error}") 181 | return False 182 | 183 | async def dalle3(self, prompt): # 返回生成的图片的绝对路径,报错的话返回错误 184 | logger.info(f"开始生成图片: {prompt}") 185 | client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) 186 | try: 187 | image_generation = await client.images.generate( 188 | prompt=prompt, 189 | model=self.model_name, 190 | n=1, 191 | response_format="b64_json", 192 | quality=self.image_quality, 193 | size=self.image_size) 194 | 195 | image_b64decode = base64.b64decode(image_generation.data[0].b64_json) 196 | save_path = os.path.abspath(f"resources/cache/dalle3_{time.time_ns()}.png") 197 | with open(save_path, "wb") as f: 198 | f.write(image_b64decode) 199 | except Exception as e: 200 | return e 201 | 202 | logger.info(f"生成图片 {prompt} 成功: {save_path}") 203 | return save_path 204 | 205 | def senstitive_word_check(self, message): # 检查敏感词 206 | for word in self.sensitive_words: 207 | if word in message: 208 | return False 209 | return True 210 | 211 | async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): 212 | if recv.from_group(): # 判断是群还是私聊 213 | out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" 214 | logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') 215 | bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 216 | 217 | else: 218 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 219 | bot.send_text(out_message, recv.roomid) # 发送 220 | -------------------------------------------------------------------------------- /plugins/mention/mention_gpt.yml: -------------------------------------------------------------------------------- 1 | gpt_point_price: 3 2 | 3 | # 调用函数需要特定的版本 https://platform.openai.com/docs/guides/function-calling#which-models-support-function-calling 4 | gpt_version: 'gpt-4o-mini' 5 | gpt_max_token: 1000 6 | gpt_temperature: 0.5 7 | 8 | # 每次生成图片消耗的积分 9 | image_price: 5 10 | 11 | # https://platform.openai.com/docs/api-reference/images/create 12 | model_name: "dall-e-3" # 模型名 13 | image_quality: "standard" # 生成质量,更好的质量可设置为"hd" 14 | image_size: "1024x1024" # 图片大小 15 | image_style: "vivid" # 图片风格 -------------------------------------------------------------------------------- /plugins/text/private_chatgpt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import re 6 | 7 | import yaml 8 | from loguru import logger 9 | from openai import AsyncOpenAI 10 | from wcferry import client 11 | 12 | from utils.database import BotDatabase 13 | from utils.plugin_interface import PluginInterface 14 | from wcferry_helper import XYBotWxMsg 15 | 16 | 17 | class private_chatgpt(PluginInterface): 18 | def __init__(self): 19 | config_path = "plugins/text/private_chatgpt.yml" 20 | 21 | with open(config_path, "r", encoding="utf-8") as f: 22 | config = yaml.safe_load(f.read()) 23 | 24 | self.enable_private_chat_gpt = config["enable_private_chat_gpt"] # 是否开启私聊chatgpt 25 | 26 | self.gpt_version = config["gpt_version"] # gpt版本 27 | self.gpt_max_token = config["gpt_max_token"] # gpt 最大token 28 | self.gpt_temperature = config["gpt_temperature"] # gpt 温度 29 | 30 | self.private_chat_gpt_price = config["private_chat_gpt_price"] # 私聊gpt使用价格(单次) 31 | self.dialogue_count = config["dialogue_count"] # 保存的对话轮数 32 | self.clear_dialogue_keyword = config["clear_dialogue_keyword"] 33 | 34 | main_config_path = "main_config.yml" 35 | with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 36 | main_config = yaml.safe_load(f.read()) 37 | 38 | self.admins = main_config["admins"] # 管理员列表 39 | 40 | self.openai_api_base = main_config["openai_api_base"] # openai api 链接 41 | self.openai_api_key = main_config["openai_api_key"] # openai api 密钥 42 | 43 | sensitive_words_path = "sensitive_words.yml" # 加载敏感词yml 44 | with open(sensitive_words_path, "r", encoding="utf-8") as f: # 读取设置 45 | sensitive_words_config = yaml.safe_load(f.read()) 46 | self.sensitive_words = sensitive_words_config["sensitive_words"] # 敏感词列表 47 | 48 | self.db = BotDatabase() 49 | 50 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 51 | if not self.enable_private_chat_gpt: 52 | return # 如果不开启私聊chatgpt,不处理 53 | elif recv.from_group(): 54 | return # 如果是群聊消息,不处理 55 | 56 | recv.content = re.split(" |\u2005", recv.content) # 拆分消息 57 | 58 | # 这里recv.content中的内容是分割的 59 | gpt_request_message = " ".join(recv.content) 60 | wxid = recv.sender 61 | 62 | if gpt_request_message.startswith("我是"): # 微信打招呼消息,不需要处理 63 | return 64 | 65 | if wxid == "weixin": # 微信系统消息, 不处理 66 | return 67 | 68 | error = '' 69 | if self.db.get_points(wxid) < self.private_chat_gpt_price and wxid not in self.admins and not self.db.get_whitelist(wxid): # 积分不够 70 | error = f"您的积分不足 {self.private_chat_gpt_price} 点,无法使用私聊GPT功能!⚠️" 71 | elif not self.senstitive_word_check(gpt_request_message): # 有敏感词 72 | error = "您的问题中包含敏感词,请重新输入!⚠️" 73 | 74 | if not error: # 如果没有错误 75 | if recv.content[0] in self.clear_dialogue_keyword: # 如果是清除对话记录的关键词,清除数据库对话记录 76 | self.clear_dialogue(wxid) # 保存清除了的数据到数据库 77 | out_message = "对话记录已清除!✅" 78 | bot.send_text(out_message, wxid) 79 | logger.info(f'[发送信息]{out_message}| [发送到] {wxid}') 80 | else: 81 | gpt_answer = await self.chatgpt(wxid, gpt_request_message) # 调用chatgpt函数 82 | if gpt_answer[0]: # 如果没有错误 83 | bot.send_text(gpt_answer[1], wxid) # 发送回答 84 | logger.info(f'[发送信息]{gpt_answer[1]}| [发送到] {wxid}') 85 | if wxid not in self.admins and not self.db.get_whitelist(wxid): 86 | self.db.add_points(wxid, -self.private_chat_gpt_price) # 扣除积分,管理员不扣 87 | else: 88 | out_message = f"出现错误⚠️!\n{gpt_answer[1]}" # 如果有错误,发送错误信息 89 | bot.send_text(out_message, wxid) 90 | logger.error(f'[发送信息]{out_message}| [发送到] {wxid}') 91 | else: 92 | bot.send_text(error, recv.roomid) 93 | logger.info(f'[发送信息]{error}| [发送到] {wxid}') 94 | 95 | async def chatgpt(self, wxid: str, message: str): # 这个函数请求了openai的api 96 | request_content = self.compose_gpt_dialogue_request_content(wxid, message) # 构成对话请求内容,返回一个包含之前对话的列表 97 | 98 | client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) 99 | try: 100 | chat_completion = await client.chat.completions.create( 101 | messages=request_content, 102 | model=self.gpt_version, 103 | temperature=self.gpt_temperature, 104 | max_tokens=self.gpt_max_token, 105 | ) # 调用openai api 106 | 107 | self.save_gpt_dialogue_request_content(wxid, request_content, 108 | chat_completion.choices[0].message.content) # 保存对话请求与回答内容 109 | return True, chat_completion.choices[0].message.content # 返回对话回答内容 110 | except Exception as error: 111 | return False, error 112 | 113 | def compose_gpt_dialogue_request_content(self, wxid: str, new_message: str) -> list: 114 | json_data = self.db.get_private_gpt_data(wxid) # 从数据库获得到之前的对话 115 | 116 | if not json_data or "data" not in json_data.keys(): # 如果没有对话数据,则初始化 117 | init_data = {"data": []} 118 | json_data = init_data 119 | 120 | previous_dialogue = json_data['data'][self.dialogue_count * -2:] # 获取指定轮数的对话,乘-2是因为一轮对话包含了1个请求和1个答复 121 | request_content = [{"role": "system", "content": "You are a helpful assistant that output in plain text."}] 122 | request_content += previous_dialogue # 将之前的对话加入到api请求内容中 123 | 124 | request_content.append({"role": "user", "content": new_message}) # 将用户新的问题加入api请求内容 125 | 126 | return request_content 127 | 128 | def save_gpt_dialogue_request_content(self, wxid: str, request_content: list, gpt_response: str) -> None: 129 | request_content.append({"role": "assistant", "content": gpt_response}) # 将gpt回答加入到api请求内容 130 | request_content = request_content[self.dialogue_count * -2:] # 将列表切片以符合指定的对话轮数,乘-2是因为一轮对话包含了1个请求和1个答复 131 | 132 | json_data = {"data": request_content} # 构成保存需要的json数据 133 | self.db.save_private_gpt_data(wxid, json_data) # 保存到数据库中 134 | 135 | def senstitive_word_check(self, message): # 检查敏感词 136 | for word in self.sensitive_words: 137 | if word in message: 138 | return False 139 | return True 140 | 141 | def clear_dialogue(self, wxid): # 清除对话记录 142 | self.db.save_private_gpt_data(wxid, {"data": []}) 143 | -------------------------------------------------------------------------------- /plugins/text/private_chatgpt.yml: -------------------------------------------------------------------------------- 1 | # 是否开启私聊gpt 2 | enable_private_chat_gpt: True # 可为 True 或者 False 3 | 4 | dialogue_count: 3 # 上下文记忆轮数,0为关闭。连续对话消耗的token较多,谨慎设置! 5 | private_chat_gpt_price: 3 # 每次对话消耗的积分,0为关闭 6 | 7 | clear_dialogue_keyword: [ "清除对话", "重置对话", "新建对话","清除对话记录" ] 8 | 9 | # gpt请求模型设置 10 | gpt_version: 'gpt-4o-mini' # 连续对话消耗的token较多,谨慎设置! 11 | gpt_max_token: 1000 12 | gpt_temperature: 0.5 -------------------------------------------------------------------------------- /plugins/voice/_voice_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | from loguru import logger 6 | from wcferry import client 7 | 8 | from utils.plugin_interface import PluginInterface 9 | from wcferry_helper import XYBotWxMsg 10 | 11 | 12 | class voice_test(PluginInterface): 13 | def __init__(self): 14 | pass 15 | 16 | async def run(self, bot: client.Wcf, recv: XYBotWxMsg): 17 | logger.debug(f"收到语音消息!{recv}") 18 | bot.send_text(str(recv), recv.roomid) 19 | bot.send_file(recv.voice, recv.roomid) 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | schedule~=1.2.2 2 | PyYAML~=6.0.1 3 | loguru~=0.7.2 4 | wcferry~=39.2.4.0 5 | pytz~=2024.1 6 | requests~=2.32.0 7 | openai~=1.35.14 8 | aiohttp~=3.10.11 9 | beautifulsoup4~=4.12.3 10 | pillow~=10.3.0 11 | captcha~=0.5.0 12 | openpyxl~=3.1.5 13 | pynng~=0.8.0 14 | xmltodict~=0.13.0 -------------------------------------------------------------------------------- /resources/gomoku_board_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/resources/gomoku_board_original.png -------------------------------------------------------------------------------- /sensitive_words.yml: -------------------------------------------------------------------------------- 1 | sensitive_words: [ "敏感词1","敏感词2" ] -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | 4 | import pynng 5 | import schedule 6 | import yaml 7 | from loguru import logger 8 | from wcferry import wcf_pb2, WxMsg 9 | 10 | import utils.xybot as xybot 11 | from utils.plans_manager import plan_manager 12 | from utils.plugin_manager import plugin_manager 13 | from wcferry_helper import * 14 | 15 | 16 | async def recv_msg_async(sock, rsp): 17 | loop = asyncio.get_running_loop() 18 | try: 19 | message = await loop.run_in_executor(None, sock.recv_msg, True) 20 | rsp.ParseFromString(message.bytes) 21 | except Exception as e: 22 | raise Exception(f"接受消息失败:{e}") 23 | return rsp.wxmsg 24 | 25 | 26 | def callback(worker): # 处理线程结束时,有无错误 27 | worker_exception = worker.exception() 28 | if worker_exception: 29 | logger.error(worker_exception) 30 | 31 | 32 | async def plan_run_pending(): # 计划等待判定线程 33 | while True: 34 | schedule.run_pending() 35 | await asyncio.sleep(1) 36 | 37 | 38 | def is_port_in_use(ip: str, port: int): 39 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 40 | return s.connect_ex((ip, port)) == 0 41 | 42 | 43 | async def main(): 44 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 45 | 46 | # ---- log设置 读取设置 ---- # 47 | logger.add( 48 | "logs/log_{time}.log", 49 | format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", 50 | encoding="utf-8", 51 | enqueue=True, 52 | retention="2 weeks", 53 | rotation="00:01", 54 | ) # 日志设置 55 | logger.info("已设置日志") 56 | 57 | cache_path = "resources/cache" # 检测是否有cache文件夹 58 | if not os.path.exists(cache_path): 59 | logger.info("检测到未创建cache缓存文件夹") 60 | os.makedirs(cache_path) 61 | logger.info("已创建cache文件夹") 62 | 63 | with open("main_config.yml", "r", encoding="utf-8") as f: # 读取设置 64 | config = yaml.safe_load(f.read()) 65 | 66 | ip = config["ip"] 67 | port = config["port"] 68 | 69 | logger.info("读取设置成功") 70 | 71 | # ---- 微信Hook注入 修复微信版本过低问题 机器人实例化 登陆监测 机器人启动 ---- # 72 | 73 | # 注入 74 | if ip == "127.0.0.1" or ip == "localhost": 75 | if not is_port_in_use(ip, port): 76 | logger.info("开始注入Wcferry") 77 | inject(port, debug=True, local=True) 78 | if is_port_in_use(ip, port): 79 | logger.success("Wcferry注入成功") 80 | else: 81 | logger.error("Wcferry注入失败") 82 | return 83 | 84 | else: 85 | logger.success("Wcferry已被注入") 86 | else: 87 | logger.success("检测到远程调试,无需注入") 88 | 89 | # 实例化 90 | logger.debug(f"IP: {ip}, 端口: {port}") 91 | bot = client.Wcf(ip, port, debug=False, block=False) 92 | logger.info("机器人实例化成功") 93 | 94 | # 检查是否登陆了微信 95 | logger.info("开始检测微信是否登陆") 96 | if not bot.is_login(): 97 | logger.warning("机器人微信账号未登录!请扫码登陆微信。") 98 | while not bot.is_login(): 99 | await asyncio.sleep(1) 100 | 101 | logger.debug(f"微信账号: {bot.get_self_wxid()}") 102 | 103 | logger.success("已确认微信已登陆,开始启动XYBot") 104 | handlebot = xybot.XYBot(bot) 105 | 106 | # ---- 加载插件 加载计划 ---- # 107 | 108 | # 加载所有插件 109 | plugin_manager.load_plugins() # 加载所有插件 110 | logger.success("已加载所有插件") 111 | 112 | plans_dir = "plans" 113 | plan_manager.load_plans(bot, plans_dir) # 加载所有计划 114 | 115 | asyncio.create_task(plan_run_pending()).add_done_callback(callback) # 开启计划等待判定线程 116 | logger.success("已加载所有计划,并开始后台运行") 117 | 118 | logger.debug(bot.get_msg_types()) 119 | 120 | # ---- 开始接受处理消息 ---- # 121 | req = wcf_pb2.Request() 122 | req.func = wcf_pb2.FUNC_ENABLE_RECV_TXT 123 | bot._send_request(req) 124 | 125 | await asyncio.sleep(5) # 等待微信消息接受准备 126 | 127 | rsp = wcf_pb2.Response() 128 | 129 | with pynng.Pair1() as sock: 130 | sock.dial(bot.msg_url, block=True) 131 | logger.success(f"连接成功: {bot.msg_url}") 132 | 133 | logger.info("开始接受消息") 134 | while True: 135 | message = await recv_msg_async(sock, rsp) 136 | asyncio.create_task(handlebot.message_handler(bot, WxMsg(message))).add_done_callback(callback) 137 | 138 | 139 | if __name__ == "__main__": 140 | asyncio.run(main()) 141 | -------------------------------------------------------------------------------- /utils/plans_interface.py: -------------------------------------------------------------------------------- 1 | class PlansInterface: 2 | def run(self, bot): 3 | raise NotImplementedError("Subclasses must implement the 'run' method.") 4 | -------------------------------------------------------------------------------- /utils/plans_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import importlib 6 | import os 7 | 8 | from loguru import logger 9 | from wcferry import client 10 | 11 | from utils.plans_interface import PlansInterface 12 | from utils.singleton import singleton 13 | 14 | 15 | @singleton 16 | class PlansManager: 17 | def __init__(self): 18 | self.plans = {} 19 | 20 | def load_plan(self, bot: client.Wcf, plan_name): 21 | if plan_name not in self.plans: 22 | module = importlib.import_module(f"plans.{plan_name}") 23 | plan_class = getattr(module, plan_name) 24 | if issubclass(plan_class, PlansInterface): 25 | plan_cinstance = plan_class() 26 | self.plans[plan_name] = plan_cinstance 27 | self.plans[plan_name].run(bot) 28 | logger.info(f"+ 已加载计划:{plan_name}") 29 | else: 30 | logger.error(f"计划{plan_name}不是PlansInterface的子类") 31 | 32 | def load_plans(self, bot: client.Wcf, plan_dir): 33 | logger.info("开始加载所有计划") 34 | for plan_file in os.listdir(plan_dir): 35 | if plan_file.endswith(".py") and plan_file != "__init__.py" and not plan_file.startswith("_"): 36 | plan_name = os.path.splitext(plan_file)[0] 37 | self.load_plan(bot, plan_name) 38 | 39 | def unload_plan(self, plan_name): 40 | if plan_name in self.plans: 41 | del self.plans[plan_name] 42 | logger.info(f"- 已卸载计划{plan_name}") 43 | 44 | 45 | # 实例化插件管理器 46 | plan_manager = PlansManager() 47 | -------------------------------------------------------------------------------- /utils/plugin_interface.py: -------------------------------------------------------------------------------- 1 | class PluginInterface: 2 | def run(self, bot, recv): 3 | raise NotImplementedError("Subclasses must implement the 'run' method.") 4 | -------------------------------------------------------------------------------- /utils/plugin_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import importlib 6 | import os 7 | import sys 8 | 9 | import yaml 10 | from loguru import logger 11 | 12 | from utils.plugin_interface import PluginInterface 13 | from utils.singleton import singleton 14 | 15 | 16 | @singleton 17 | class PluginManager: 18 | def __init__(self): 19 | self.plugins = {"command": {}, "text": {}, "mention": {}, "image": {}, "voice": {}, "join_group": {}} 20 | self.keywords = {} 21 | 22 | with open("main_config.yml", "r", encoding="utf-8") as f: # 读取设置 23 | config = yaml.safe_load(f.read()) 24 | 25 | self.excluded_plugins = config["excluded_plugins"] 26 | 27 | self.all_plugin_types = ["command", "text", "mention", "image", "voice", "join_group"] 28 | 29 | def refresh_keywords(self): 30 | """ 31 | 刷新关键词。Refresh the keywords. 32 | :return: (bool, str) - 如果刷新成功,返回True和成功消息。如果刷新失败,返回False和失败原因 (bool, str) - True and a success message if the keywords were refreshed successfully, False and an error message otherwise. 33 | """ 34 | self.keywords.clear() 35 | plugins_folder = "plugins/command" 36 | 37 | # 遍历文件夹中的所有文件 38 | for root, dirs, files in os.walk(plugins_folder): 39 | for file in files: 40 | if file.endswith(".yml") and not file.startswith("_"): 41 | # 处理符合条件的文件 42 | file_path = os.path.join(root, file) 43 | 44 | with open(file_path, "r", encoding="utf-8") as f: # 读取设置 45 | config = yaml.safe_load(f.read()) 46 | 47 | keywords_list = config["keywords"] 48 | plugin_name = config["plugin_name"] 49 | 50 | if plugin_name in self.plugins["command"].keys(): 51 | for keyword in keywords_list: 52 | self.keywords[keyword] = plugin_name 53 | 54 | logger.info("已刷新指令关键词") 55 | return True, "成功" 56 | 57 | def get_keywords(self): 58 | return self.keywords 59 | 60 | def load_plugin(self, plugin_name: str, no_refresh: bool = False, log: bool = True): 61 | """ 62 | 按插件名称加载插件。插件必须是PluginInterface的子类。 63 | Loads a plugin by its name. The plugin must be a subclass of PluginInterface. 64 | 65 | :param plugin_name: 要加载的插件的名称。The name of the plugin to load. 66 | :param no_refresh: 是否刷新关键词。Whether to refresh the keywords. 67 | :param log: 是否进行日志。Whether to log. 68 | :return: tuple -(bool, str) - 如果加载成功,返回True和成功消息。如果加载失败,返回False和失败原因 (bool, str) - True and a success message if the plugin was loaded successfully, False and an error message otherwise. 69 | """ 70 | if plugin_name in self.plugins: 71 | logger.warning(f"! 未加载插件:{plugin_name},因为它已经加载") 72 | return False, f"插件 {plugin_name} 已经加载。" 73 | 74 | # 判断插件是否已经加载 75 | for plugin_type in self.all_plugin_types: 76 | if plugin_name in self.plugins[plugin_type].keys(): 77 | logger.warning(f"! 未加载插件:{plugin_name},因为它已经加载") 78 | return False, f"插件 {plugin_name} 已经加载。" 79 | 80 | for plugin_type in self.all_plugin_types: # 遍历所有插件文件夹 81 | if f"{plugin_name}.py" in os.listdir(f'plugins/{plugin_type}'): # 判断插件是否存在 82 | module = importlib.import_module(f"plugins.{plugin_type}.{plugin_name}") # 导入插件 83 | plugin_class = getattr(module, plugin_name) # 获取插件类 84 | if issubclass(plugin_class, PluginInterface): # 判断插件是否是PluginInterface的子类 85 | plugin_instance = plugin_class() 86 | self.plugins[plugin_type][plugin_name] = plugin_instance # 将插件实例存入插件字典 87 | if log: 88 | logger.info(f"+ 已加载插件:{plugin_name}") 89 | if not no_refresh: 90 | self.refresh_keywords() 91 | return True, "成功" # 如果插件加载成功则返回True 92 | 93 | else: 94 | logger.warning(f"! 未加载插件:{plugin_name},因为它不是PluginInterface的子类") 95 | return False, f"插件 {plugin_name} 不是 PluginInterface 的子类。" 96 | 97 | logger.warning(f"! 未加载插件:{plugin_name},因为它不存在") 98 | return False, f"插件 {plugin_name} 不存在。" 99 | 100 | def load_plugins(self): 101 | """ 102 | 加载所有插件。Load all plugins. 103 | :return: (bool, str) - 如果加载成功,返回True和成功消息。如果加载失败,返回False和失败原因 (bool, str) - True and a success message if the plugins were loaded successfully, False and an error message otherwise. 104 | """ 105 | logger.info("开始加载所有插件") 106 | 107 | for plugin_type in self.all_plugin_types: 108 | for plugin_file in os.listdir(f"plugins/{plugin_type}"): 109 | if plugin_file.endswith(".py") and not plugin_file.startswith("_"): 110 | plugin_name = os.path.splitext(plugin_file)[0] 111 | 112 | if plugin_name in self.excluded_plugins: 113 | logger.info(f"! 未加载插件:{plugin_name},因为它在排除列表中") 114 | 115 | else: 116 | module = importlib.import_module(f"plugins.{plugin_type}.{plugin_name}") # 导入插件 117 | plugin_class = getattr(module, plugin_name) # 获取插件类 118 | if issubclass(plugin_class, PluginInterface): # 判断插件是否是PluginInterface的子类 119 | plugin_instance = plugin_class() 120 | self.plugins[plugin_type][plugin_name] = plugin_instance # 将插件实例存入插件字典 121 | logger.info(f"+ 已加载插件:{plugin_name}") 122 | 123 | else: 124 | logger.error(f"! 未加载插件:{plugin_name},因为它不是PluginInterface的子类") 125 | 126 | self.refresh_keywords() 127 | return True, "成功" 128 | 129 | def unload_plugin(self, plugin_name, no_refresh: bool = False): 130 | """ 131 | 卸载插件。Unload a plugin. 132 | :param plugin_name: 插件名。The name of the plugin to unload. 133 | :param no_refresh: 是否刷新关键词。Whether to refresh the keywords. 134 | :return: tuple -(bool, str) - 如果卸载成功,返回True和成功消息。如果卸载失败,返回False和失败原因 (bool, str) - True and a success message if the plugin was unloaded successfully, False and an error message otherwise. 135 | """ 136 | if plugin_name == "manage_plugins": 137 | logger.info("! 禁止卸载插件:manage_plugins") 138 | return False, "禁止卸载manage_plugins" 139 | 140 | for plugin_type in self.all_plugin_types: 141 | if plugin_name in self.plugins[plugin_type].keys(): 142 | del self.plugins[plugin_type][plugin_name] 143 | del sys.modules[f"plugins.{plugin_type}.{plugin_name}"] 144 | logger.info(f"- 已卸载插件:{plugin_name}") 145 | 146 | if not no_refresh: 147 | self.refresh_keywords() 148 | 149 | return True, "成功" # 如果插件卸载成功则返回True 150 | 151 | logger.info(f"! 未找到插件:{plugin_name},无法卸载") 152 | return False, f"未找到插件:{plugin_name}" # 如果插件不存在则返回False 153 | 154 | def unload_plugins(self): 155 | """ 156 | 卸载所有插件。Unload all plugins. 157 | :return: tuple -(bool, str) - 如果卸载成功,返回True和成功消息。如果卸载失败,返回False和失败原因 (bool, str) - True and a success message if the plugins were unloaded successfully, False and an error message otherwise. 158 | """ 159 | logger.info("开始卸载所有插件") 160 | for plugin_type in self.all_plugin_types: 161 | for plugin_name in list(self.plugins[plugin_type].keys()): 162 | if plugin_name != "manage_plugins": 163 | del self.plugins[plugin_type][plugin_name] 164 | del sys.modules[f"plugins.{plugin_type}.{plugin_name}"] 165 | logger.info(f"- 已卸载插件:{plugin_name}") 166 | self.refresh_keywords() 167 | return True, "成功" 168 | 169 | def reload_plugin(self, plugin_name): 170 | """ 171 | 重载插件。Reload a plugin. 172 | :param plugin_name: 插件名。The name of the plugin to reload. 173 | :return: (bool, str) - 如果重载成功,返回True和成功消息。如果重载失败,返回False和失败原因 (bool, str) - True and a success message if the plugin was reloaded successfully, False and an error message otherwise. 174 | """ 175 | if plugin_name == "manage_plugins": 176 | logger.info("! 禁止重载插件:manage_plugins") 177 | return False, "禁止重载manage_plugins" 178 | 179 | for plugin_type in self.all_plugin_types: 180 | if plugin_name in list(self.plugins[plugin_type].keys()): 181 | # 卸载 182 | del self.plugins[plugin_type][plugin_name] 183 | del sys.modules[f"plugins.{plugin_type}.{plugin_name}"] 184 | 185 | # 加载 186 | module = importlib.import_module(f"plugins.{plugin_type}.{plugin_name}") # 导入插件 187 | plugin_class = getattr(module, plugin_name) # 获取插件类 188 | if issubclass(plugin_class, PluginInterface): # 判断插件是否是PluginInterface的子类 189 | plugin_instance = plugin_class() 190 | self.plugins[plugin_type][plugin_name] = plugin_instance # 将插件实例存入插件字典 191 | 192 | self.refresh_keywords() 193 | logger.info(f"+ 已重载插件:{plugin_name}") 194 | return True, "成功" 195 | 196 | else: 197 | logger.error(f"! 未重载插件:{plugin_name},因为它不是PluginInterface的子类") 198 | return False, f"插件 {plugin_name} 不是 PluginInterface 的子类。" 199 | 200 | logger.error(f"! 未重载插件:{plugin_name},因为它不存在") 201 | return False, f"插件 {plugin_name} 不存在。" 202 | 203 | def reload_plugins(self): 204 | """ 205 | 重载所有插件。Reload all plugins. 206 | :return: (bool, str) - 如果重载成功,返回True和成功消息。如果重载失败,返回False和失败原因 (bool, str) - True and a success message if the plugins were reloaded successfully, False and an error message otherwise. 207 | """ 208 | logger.info("开始重载所有插件") 209 | for plugin_type in self.all_plugin_types: 210 | for plugin_name in list(self.plugins[plugin_type].keys()): 211 | if plugin_name != "manage_plugins": 212 | # 卸载 213 | del self.plugins[plugin_type][plugin_name] 214 | del sys.modules[f"plugins.{plugin_type}.{plugin_name}"] 215 | 216 | # 加载 217 | module = importlib.import_module(f"plugins.{plugin_type}.{plugin_name}") 218 | plugin_class = getattr(module, plugin_name) 219 | if issubclass(plugin_class, PluginInterface): 220 | plugin_instance = plugin_class() 221 | self.plugins[plugin_type][plugin_name] = plugin_instance 222 | logger.info(f"+ 已重载插件:{plugin_name}") 223 | else: 224 | logger.error(f"! 未重载插件:{plugin_name},因为它不是PluginInterface的子类") 225 | return True, "成功" 226 | 227 | 228 | # 实例化插件管理器 229 | plugin_manager = PluginManager() 230 | -------------------------------------------------------------------------------- /utils/singleton.py: -------------------------------------------------------------------------------- 1 | def singleton(cls): 2 | _instance = {} 3 | 4 | def inner(): 5 | if cls not in _instance: 6 | _instance[cls] = cls() 7 | return _instance[cls] 8 | 9 | return inner 10 | -------------------------------------------------------------------------------- /utils/xybot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import asyncio 6 | import os 7 | import re 8 | 9 | import yaml 10 | from loguru import logger 11 | from wcferry import client, wxmsg 12 | 13 | from utils.database import BotDatabase 14 | from utils.plugin_manager import plugin_manager 15 | from wcferry_helper import XYBotWxMsg, async_download_image 16 | 17 | 18 | class XYBot: 19 | def __init__(self, bot: client.Wcf): 20 | with open("main_config.yml", "r", encoding="utf-8") as f: # 读取设置 21 | main_config = yaml.safe_load(f.read()) 22 | self.command_prefix = main_config["command_prefix"] # 命令前缀 23 | logger.debug(f"指令前缀为(如果是空则不会显示): {self.command_prefix}") 24 | 25 | self.keywords = plugin_manager.get_keywords() 26 | 27 | self.ignorance_mode = main_config['mode'] 28 | self.ignorance_blacklist = main_config['blacklist'] 29 | self.ignorance_whitelist = main_config['whitelist'] 30 | 31 | self.image_save_path = os.path.abspath("resources/cache") 32 | self.voice_save_path = os.path.abspath("resources/cache") 33 | logger.debug(f"语音保存路径: {self.voice_save_path}") 34 | logger.debug(f"图片保存路径: {self.image_save_path}") 35 | 36 | self.self_wxid = bot.get_self_wxid() 37 | 38 | async def message_handler(self, bot: client.Wcf, recv: wxmsg.WxMsg) -> None: 39 | recv = XYBotWxMsg(recv) 40 | 41 | # 尝试设置用户的昵称 42 | db = BotDatabase() 43 | 44 | nickname_latest = False 45 | if not db.get_nickname(recv.sender): # 如果数据库中没有这个用户的昵称,需要先获取昵称再运行插件 46 | await self.attempt_set_nickname(bot, recv, db) 47 | nickname_latest = True 48 | 49 | message_type = recv.type 50 | if message_type == 1: # 是文本消息 51 | await self.text_message_handler(bot, recv) 52 | elif message_type == 3: # 是图片消息 53 | await self.image_message_handler(bot, recv) 54 | elif message_type == 34: # 语音消息 55 | await self.voice_message_handler(bot, recv) 56 | elif message_type == 10000: 57 | await self.system_message_handler(bot, recv) 58 | elif message_type == 47: # 表情消息 59 | await self.emoji_message_handler(recv) 60 | else: # 其他消息,type不存在或者还未知干啥用的 61 | logger.info(f"[其他消息] {recv}") 62 | 63 | if not nickname_latest: 64 | await self.attempt_set_nickname(bot, recv, db) 65 | 66 | async def attempt_set_nickname(self, bot: client.Wcf, recv: XYBotWxMsg, db: BotDatabase) -> None: 67 | if recv.from_group(): # 如果是群聊 68 | nickname = bot.get_alias_in_chatroom(recv.sender, recv.roomid) 69 | db.set_nickname(recv.sender, nickname) 70 | else: # 如果是私聊 71 | flag = True 72 | for user in bot.contacts: 73 | if user["wxid"] == recv.sender: 74 | db.set_nickname(recv.sender, user["name"]) 75 | flag = False 76 | break 77 | 78 | if flag: # 如果没有找到,重新获取一次最新的联系人列表 79 | for user in bot.get_contacts(): 80 | if user["wxid"] == recv.sender: 81 | db.set_nickname(recv.sender, user["name"]) 82 | break 83 | 84 | async def text_message_handler(self, bot: client.Wcf, recv: XYBotWxMsg) -> None: 85 | logger.info(f"[收到文本消息]:{recv}") 86 | 87 | if not self.ignorance_check(recv): # 屏蔽检查 88 | return 89 | 90 | # @机器人处理 91 | if self.self_wxid in recv.ats: # 机器人被@,调用所有mention插件 92 | for plugin in plugin_manager.plugins["mention"].values(): 93 | await asyncio.create_task(plugin.run(bot, recv)) 94 | return 95 | 96 | # 指令处理 97 | if recv.content.startswith(self.command_prefix) or self.command_prefix == "": 98 | if self.command_prefix != "": # 特殊处理,万一用户想要使用空前缀 99 | recv.content = recv.content[1:] # 去除命令前缀 100 | 101 | recv_keyword = recv.content.split(" |\u2005")[0] # 获取命令关键词 102 | for keyword in plugin_manager.get_keywords().keys(): # 遍历所有关键词 103 | if re.match(keyword, recv_keyword): # 如果正则匹配到了,执行插件run函数 104 | plugin_func = plugin_manager.keywords[keyword] 105 | await asyncio.create_task(plugin_manager.plugins["command"][plugin_func].run(bot, recv)) 106 | return 107 | 108 | if recv.from_group() and self.command_prefix != "": # 不是指令但在群里 且设置了指令前缀 109 | out_message = "该指令不存在!⚠️" 110 | logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') 111 | bot.send_text(out_message, recv.roomid) 112 | return # 执行完后直接返回 113 | 114 | # 普通消息处理 115 | for plugin in plugin_manager.plugins["text"].values(): 116 | await asyncio.create_task(plugin.run(bot, recv)) 117 | 118 | async def image_message_handler(self, bot: client.Wcf, recv: XYBotWxMsg) -> None: 119 | logger.info(f"[收到图片消息]{recv}") 120 | 121 | if not self.ignorance_check(recv): # 屏蔽检查 122 | return 123 | 124 | # 如果是图片消息,recv字典中会有一个image键值对,值为图片的绝对路径。 125 | path = await async_download_image(bot, recv.id, recv.extra, self.image_save_path) 126 | recv.image = os.path.abspath(path) # 确保图片为绝对路径 127 | 128 | # image插件调用 129 | for plugin in plugin_manager.plugins["image"].values(): 130 | await asyncio.create_task(plugin.run(bot, recv)) 131 | 132 | async def voice_message_handler(self, bot: client.Wcf, recv: XYBotWxMsg) -> None: 133 | logger.info(f"[收到语音消息]{recv}") 134 | 135 | if not self.ignorance_check(recv): # 屏蔽检查 136 | return 137 | 138 | path = await async_download_image(bot, recv.id, recv.extra, self.voice_save_path) # 下载语音 139 | recv.voice = os.path.abspath(path) # 确保语音为绝对路径 140 | 141 | # voice插件调用 142 | for plugin in plugin_manager.plugins["voice"].values(): 143 | await asyncio.create_task(plugin.run(bot, recv)) 144 | 145 | async def system_message_handler(self, bot: client.Wcf, recv: XYBotWxMsg) -> None: 146 | logger.info(f"[收到系统消息]{recv}") 147 | 148 | if not self.ignorance_check(recv): # 屏蔽检查 149 | return 150 | 151 | # 开始检查有没有群成员加入 152 | if recv.from_group(): 153 | content = recv.content 154 | result = re.findall(r'"(.*?)"加入了群聊', content) 155 | result = result[0] if result else None 156 | if result: 157 | recv.join_group = result 158 | for plugin in plugin_manager.plugins["join_group"].values(): 159 | await asyncio.create_task(plugin.run(bot, recv)) 160 | return 161 | 162 | async def emoji_message_handler(self, recv) -> None: 163 | logger.info(f"[收到表情消息]{recv}") 164 | 165 | def ignorance_check(self, recv: XYBotWxMsg) -> bool: 166 | if self.ignorance_mode == 'none': # 如果不设置屏蔽,则直接返回通过 167 | return True 168 | 169 | elif self.ignorance_mode == 'blacklist': # 如果设置了黑名单 170 | if (recv.roomid not in self.ignorance_blacklist) and (recv.sender not in self.ignorance_blacklist): 171 | return True 172 | else: 173 | return False 174 | 175 | elif self.ignorance_mode == 'whitelist': # 白名单 176 | if (recv.roomid in self.ignorance_whitelist) or (recv.sender in self.ignorance_whitelist): 177 | return True 178 | else: 179 | return False 180 | 181 | else: 182 | logger.error("未知的屏蔽模式!请检查白名单/黑名单设置!") 183 | -------------------------------------------------------------------------------- /wcferry_helper/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | from .wcferry_helper import * 6 | -------------------------------------------------------------------------------- /wcferry_helper/injector.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/wcferry_helper/injector.exe -------------------------------------------------------------------------------- /wcferry_helper/injector/go.mod: -------------------------------------------------------------------------------- 1 | module injector 2 | 3 | go 1.21.12 4 | -------------------------------------------------------------------------------- /wcferry_helper/injector/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "strconv" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | const lib_sdk = "sdk.dll" 13 | const func_inject = "WxInitSDK" 14 | const func_destroy = "WxDestroySDK" 15 | 16 | var gbl_port int = 8001 17 | var gbl_debug bool = true 18 | var gbl_dll *syscall.DLL 19 | 20 | func log(args ...interface{}) { 21 | fmt.Println("\033[1;7;32m[Inj]\033[0m", time.Now().Format("20060102_150405"), args) 22 | } 23 | 24 | /** 初始化. 加载动态库 */ 25 | func init() { 26 | log("Load dll:", lib_sdk) 27 | var err error 28 | gbl_dll, err = syscall.LoadDLL(lib_sdk) 29 | if err != nil { panic(err) } 30 | } 31 | 32 | /** 调用库接口 */ 33 | func call_func(fun_name string, title string) { 34 | log(title) 35 | // log("Find function:", fun_name, "in dll:", gbl_dll) 36 | fun, err := gbl_dll.FindProc(fun_name) 37 | if err != nil { panic(err) } 38 | 39 | // log("Call function:", fun) 40 | dbgUintptr := uintptr(0) 41 | if gbl_debug { dbgUintptr = uintptr(1) } 42 | ret, _e, errno := syscall.Syscall(fun.Addr(), dbgUintptr, 0, uintptr(gbl_port), 0) 43 | if ret != 0 { 44 | panic("Function " + fmt.Sprint(fun) + " run failed! return:" + 45 | fmt.Sprint(ret) + ", err:" + fmt.Sprint(_e) + ", errno:" + fmt.Sprint(errno)) 46 | } 47 | } 48 | 49 | /** 监听并等待SIGINT信号 */ 50 | func waiting_signal() { 51 | sigChan := make(chan os.Signal, 1) 52 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 53 | log("Is running, press Ctrl+C to quit.") 54 | <-sigChan 55 | log("Stopped!") 56 | } 57 | 58 | func show_help(ret int) { 59 | log("Usage:", os.Args[0], "[port [debug]]") 60 | os.Exit(ret) 61 | } 62 | 63 | func main() { 64 | log("### Inject SDK into WeChat ###") 65 | argc := len(os.Args) 66 | var err error 67 | if argc > 1 { 68 | gbl_port, err = strconv.Atoi(os.Args[1]) 69 | if err != nil { show_help(1) } 70 | } 71 | if argc > 2 { 72 | gbl_debug, err = strconv.ParseBool(os.Args[2]) 73 | if err != nil { show_help(1) } 74 | } 75 | log("Set sdk port:", gbl_port, "debug:", gbl_debug) 76 | 77 | start_at := time.Now() 78 | for { 79 | if func() bool { 80 | defer func() { 81 | if r := recover(); r != nil { // 注入失败时反复重试 82 | log("Get panic:", r, " Wait for retry...") 83 | time.Sleep(3 * time.Second) 84 | } 85 | }() 86 | call_func(func_inject, "Inject SDK...") 87 | return true 88 | }() { break } 89 | } 90 | log("SDK inject success. Time used:", time.Now().Sub(start_at).Seconds()) 91 | 92 | waiting_signal() 93 | call_func(func_destroy, "SDK destroy") 94 | gbl_dll.Release() 95 | } 96 | 97 | -------------------------------------------------------------------------------- /wcferry_helper/sdk.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/wcferry_helper/sdk.dll -------------------------------------------------------------------------------- /wcferry_helper/spy.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/wcferry_helper/spy.dll -------------------------------------------------------------------------------- /wcferry_helper/spy_debug.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/wcferry_helper/spy_debug.dll -------------------------------------------------------------------------------- /wcferry_helper/wcferry_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024. Henry Yang 2 | # 3 | # This program is licensed under the GNU General Public License v3.0. 4 | 5 | import asyncio 6 | import subprocess 7 | import time 8 | from os import path 9 | from platform import system 10 | 11 | import xmltodict 12 | from wcferry import wxmsg, client 13 | 14 | 15 | def inject(port: int = 5555, debug: bool = False, local: bool = True): 16 | """ 17 | Inject WeChat Ferry into the WeChat. 18 | :param port: The port. 19 | :param debug: Whether to enable debug mode. 20 | :param local: Whether to run the injector locally. 21 | """ 22 | if not local: 23 | return 24 | 25 | sys = system() 26 | if sys == "Windows": 27 | abs_inject_dir = path.abspath("wcferry_helper") 28 | proccess = subprocess.Popen(f"injector.exe {port} {debug}", shell=True, cwd=abs_inject_dir) 29 | elif sys == "Linux": 30 | proccess = subprocess.Popen(f"wine wcferry_helper/injector.exe {port} {debug}", shell=True) 31 | else: 32 | raise NotImplementedError(f"Unsupported system: {sys}") 33 | 34 | time.sleep(10) 35 | 36 | return proccess 37 | 38 | 39 | def wxmsg_formatter(message: wxmsg.WxMsg) -> str: 40 | """ 41 | Format the received message. 42 | :param message: The received message. 43 | :return: The formatted message. 44 | """ 45 | formatted_xml = str(message.xml).replace("\n", "").replace(" ", "") 46 | formatted = f"sender:{message.sender} roomid:{message.roomid} type:{message.type} id:{message.id} content:{message.content} thumb:{message.thumb} extra:{message.extra} from_group:{message.from_group()} from_self:{message.from_self()} is_text:{message.is_text()} xml:{formatted_xml}" 47 | return formatted 48 | 49 | 50 | def wxmsg_to_dict(message: wxmsg.WxMsg) -> dict: 51 | """ 52 | Convert the received message to a dictionary. 53 | :param message: The received message. 54 | :return: The dictionary. 55 | """ 56 | dictionary = { 57 | "sender": message.sender, 58 | "roomid": message.roomid, 59 | "type": message.type, 60 | "id": message.id, 61 | "content": message.content, 62 | "thumb": message.thumb, 63 | "extra": message.extra, 64 | "from_group": message.from_group(), 65 | "from_self": message.from_self(), 66 | "is_text": message.is_text(), 67 | "is_at": message.is_at, 68 | "xml": message.xml 69 | } 70 | return dictionary 71 | 72 | 73 | class XYBotWxMsg: 74 | def __init__(self, msg: wxmsg.WxMsg): 75 | self._is_self = msg.from_self() 76 | self._is_group = msg.from_group() 77 | self.type = msg.type 78 | self.id = msg.id 79 | self.ts = msg.ts 80 | self.sign = msg.sign 81 | self.xml = msg.xml 82 | self.sender = msg.sender 83 | self.roomid = msg.roomid 84 | self.content = msg.content 85 | self.thumb = msg.thumb 86 | self.extra = msg.extra 87 | self.ats = [] 88 | self.image = "" 89 | self.voice = "" 90 | self.join_group = "" 91 | 92 | # 处理xml 93 | self.xml = xmltodict.parse(self.xml.replace('\\n|\\t| ', '')) # 将xml转换为字典 94 | 95 | # @ 信息 96 | if self.from_group(): 97 | at_user_list = self.xml.get('msgsource', {}).get('atuserlist', "") 98 | if at_user_list: 99 | self.ats = at_user_list.split(',') 100 | 101 | def __str__(self): 102 | _dict = { 103 | "sender": self.sender, 104 | "roomid": self.roomid, 105 | "type": self.type, 106 | "id": self.id, 107 | "content": self.content, 108 | "thumb": self.thumb, 109 | "extra": self.extra, 110 | "from_group": self.from_group(), 111 | "from_self": self.from_self(), 112 | "is_text": self.is_text(), 113 | "is_at": self.is_at, 114 | "xml": self.xml, 115 | "ats": self.ats, 116 | "join_group": self.join_group, 117 | } 118 | return str(_dict) 119 | 120 | def from_self(self) -> bool: 121 | """是否自己发的消息""" 122 | return self._is_self == 1 123 | 124 | def from_group(self) -> bool: 125 | """是否群聊消息""" 126 | return self._is_group 127 | 128 | def is_at(self, wxid) -> bool: 129 | """是否被 @:群消息,在 @ 名单里,并且不是 @ 所有人""" 130 | if not self.from_group(): 131 | return False # 只有群消息才能 @ 132 | 133 | if wxid not in self.ats: 134 | return False # 不在 @ 清单里 135 | 136 | if ( 137 | "@所有人" in self.content 138 | or "@all" in self.content 139 | or "@All" in self.content 140 | ): 141 | return False # 排除 @ 所有人 142 | 143 | return True 144 | 145 | def is_text(self) -> bool: 146 | """是否文本消息""" 147 | return self.type == 1 148 | 149 | 150 | async def async_download_image(bot: client.Wcf, id: int, extra: str, dir: str, timeout: int = 30) -> str: 151 | """ 152 | Download the image asynchronously. 153 | :param bot: The bot. 154 | :param id: The id. 155 | :param extra: The extra. 156 | :param dir: The directory. 157 | :param timeout: The timeout. 158 | :return: The path of the downloaded image, or an empty string if the download failed. 159 | """ 160 | loop = asyncio.get_running_loop() 161 | result = await loop.run_in_executor(None, bot.download_image, id, extra, dir, timeout) 162 | return result 163 | 164 | 165 | async def async_get_audio_msg(bot: client.Wcf, id: int, dir: str, timeout: int = 30) -> str: 166 | """ 167 | Get the audio message asynchronously. 168 | :param bot: The bot. 169 | :param id: The id. 170 | :param dir: The directory. 171 | :param timeout: The timeout. 172 | :return: The path of the audio message, or an empty string if the download failed. 173 | """ 174 | loop = asyncio.get_running_loop() 175 | result = await loop.run_in_executor(None, bot.get_audio_msg, id, dir, timeout) 176 | return result 177 | --------------------------------------------------------------------------------