├── .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 |
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 |
28 |
29 |
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 | 
83 |
84 | 随机图片
85 | 
86 |
87 | ChatGPT
88 | 
89 | 
90 |
91 | 私聊ChatGPT
92 | 
93 |
94 | 天气查询
95 | 
96 |
97 | 五子棋
98 | 
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 |
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 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HenryXiaoYang/XYBot/3865cdd8fa803eb5e61b1ee899f5b49d26a58ba6/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # XYBot 微信机器人
2 |
3 |
4 |
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 |
24 |
25 |
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 | 
79 |
80 | 随机图片
81 | 
82 |
83 | ChatGPT
84 | 
85 | 
86 |
87 | 私聊ChatGPT
88 | 
89 |
90 | 天气查询
91 | 
92 |
93 | 五子棋
94 | 
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 |
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 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------