This commit is contained in:
tamakyi
2026-09-11 17:49:34 +08:00
commit 102bb070bb
967 changed files with 5152 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
version = 1
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"
[[transformers]]
name = "ruff"
+180
View File
@@ -0,0 +1,180 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
.pdm-python
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python
# UV lock
uv.lock
+17
View File
@@ -0,0 +1,17 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 # 选择最新的稳定版本
hooks:
- id: trailing-whitespace # 去除多余的空格
- id: end-of-file-fixer # 确保文件以空行结束
- id: check-yaml # 检查 YAML 文件格式
- id: check-added-large-files # 检查提交中是否包含过大的文件
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.1
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
+65
View File
@@ -0,0 +1,65 @@
[lint]
select = [
"F", # Pyflakes
"E", # pycodestyle error
"W", # pycodestyle warning
"I", # isort
"UP", # pyupgrade
"ASYNC", # fflake8-async
"S", # flake8-bandit
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"ISC", # flake8-implicit-str-concat
"PIE", # flake8-pie
"T20", # flake8-print
"PYI", # flake8-pyi
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"RSE", # flake8-raise
"RET", # flake8-return
"SLOT", # flake8-slots
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"TCH", # flake8-type-checking
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
"ERA", # eradicate
"PL", # Pylint
"TRY", # tryceratops
"PERF", # Perflint
# "FURB", # refurb
"RUF", # Ruff-specific rules
]
ignore = [
"E402", # module-import-not-at-top-of-file
"E501", # line-too-long
"B009", # get-attr-with-constant
"B010", # set-attr-with-constant
"PLC0414", # useless-import-alias
"PLR0913", # too-many-arguments
"TRY003", # raise-vanilla-args
"RUF001", # ambiguous-unicode-character-string
"RUF002", # ambiguous-unicode-character-docstring
"RUF003", # ambiguous-unicode-character-comment
# Avoid formatter conflicts, see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
"W191", # tab-indentation
"E111", # indentation-with-invalid-multiple
"E114", # indentation-with-invalid-multiple-comment
"E117", # over-indented
"D206", # indent-with-spaces
"D300", # triple-single-quotes
"Q000", # bad-quotes-inline-string
"Q001", # bad-quotes-multiline-string
"Q002", # bad-quotes-docstring
"Q003", # avoidable-escaped-quote
"COM812", # missing-trailing-comma
"COM819", # prohibited-trailing-comma
"ISC001", # single-line-implicit-string-concatenation
"ISC002", # multi-line-implicit-string-concatenation
]
unfixable = [
"F401", # unused-import
"F841", # unused-variable
"ERA001", # commented-out-code
]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 divandia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+101
View File
@@ -0,0 +1,101 @@
<div align="center">
<a href="https://v2.nonebot.dev/store"><img src="https://github.com/Cvandia/nonebot-plugin-whateat-pic/blob/main/res/ico.jpg" width="180" height="180" alt="NoneBotPluginLogo"></a>
</div>
<div align="center">
# nonebot-plugin-whateat-pic
_⭐基于Nonebot2的一款今天吃什么喝什么的插件⭐_
</div>
<div align="center">
<a href="https://www.python.org/downloads/release/python-390/"><img src="https://img.shields.io/badge/python-3.8+-blue"></a> <a href=""><img src="https://img.shields.io/badge/QQ-1141538825-yellow"></a> <a href="https://github.com/Cvandia/nonebot-plugin-whateat-pic/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue"></a> <a href="https://v2.nonebot.dev/"><img src="https://img.shields.io/badge/Nonebot2-rc1+-red"></a>
</div>
## ⭐ 介绍
一款离线版决定今天吃喝什么的nb2插件,功能及其简单。
~~抄袭~~改编自hosinoBot的插件[今天吃什么](https://github.com/A-kirami/whattoeat)
有不足的地方还请指出
<div align="center">
### 或者你有啥关于该插件的新想法的,可以提issue或者pr (>A<)
</div>
## 💿 安装
<details>
<summary>安装</summary>
pip 安装
```
pip install nonebot-plugin-whateat-pic
```
nb-cli安装
```
nb plugin install nonebot-plugin-whateat-pic --upgrade
```
</details>
<details>
<summary>注意</summary>
由于包含有图片,包容量较大,推荐镜像站下载
清华源```https://pypi.tuna.tsinghua.edu.cn/simple```
阿里源```https://mirrors.aliyun.com/pypi/simple/```
</details>
## ⚙️ 配置
### 在env.中添加以下配置
|名称|类型|默认值|范围|说明|
|:-----:|:----:|:----:|:------:|:------|
|whateat_cd|int|10|0-9999|内置触发cd|
|whateat_max|int|0|0-9999|每日用户触发的最大次数,默认0时为无上限|
> 机器的名字“脑积水”是默认值,(我不相信有人不会配置nonebot2的nick_name)
## ⭐ 使用
### 指令:
| 指令 | 需要@ | 范围 | 说明 |权限|
|:-----:|:----:|:----:|:----:|:----:|
|今天早上吃什么|否|私聊、群聊|随机发送食物|任何|
|今天早上喝什么|否|私聊、群聊|随机发送饮品|任何|
|查看全部菜单|否|私聊、群聊|查看全部菜单|任何|
|查看菜单|否|私聊、群聊|查看指定菜单|任何|
|添加菜单|否|私聊、群聊|自定义添加菜单|群主,超管,管理员|
|删除菜单|否|私聊、群聊|自定义删除菜单|群主,超管,管理员|
**注意**
默认情况下, 您应该在指令前加上命令前缀, 通常是 /
## 🌙 未来
- [x] 或许添加更多的美食图片吧……
- [x] 添加更多功能
- [x] 自定义添加菜单,饮品(~~我懒,或者帮我写个?~~)QwQ(~~最后还是自己写~~)
- [x] 图片化```全部菜单```
- [x] 细化分类菜单,饮料(懒癌患者,>a<)
--- 喜欢记得点个star⭐---
+25
View File
@@ -0,0 +1,25 @@
from nonebot import require
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
require("nonebot_plugin_alconna")
require("nonebot_plugin_localstore")
require("nonebot_plugin_apscheduler")
from . import check_on_statup as _
from . import matcher as _ # noqa
__plugin_meta__ = PluginMetadata(
name="今天吃什么(图片版)",
description="随机发送吃的或者喝的图片",
usage="""
今天吃什么
今天喝什么
今天蜜什么
添加菜单
删除菜单
查看菜单
""",
type="application",
homepage="https://github.com/tamakyi/nonebot-plugin-whateat-pic",
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
)
@@ -0,0 +1,119 @@
import asyncio
import json
from pathlib import Path
import httpx
from nonebot import get_driver
from nonebot.log import logger
from rich.progress import Progress
from .config import config
available_urls = [
"https://raw.githubusercontent.com/tamakyi/nonebot-plugin-whateat-pic/",
"https://fastly.jsdelivr.net/gh/tamakyi/nonebot-plugin-whateat-pic@",
"https://raw.gitmirror.com/tamakyi/nonebot-plugin-whateat-pic/",
"https://ghproxy.cfd/https:/raw.githubusercontent.com/tamakyi/nonebot-plugin-whateat-pic/",
"https://ghfast.top/https:/raw.githubusercontent.com/tamakyi/nonebot-plugin-whateat-pic/",
]
async def check_resource():
semaphore = asyncio.Semaphore(10)
async def _download(client: httpx.AsyncClient, name: str):
async with semaphore:
for base_url in available_urls:
url = base_url + "refs/heads/main/res/" + name
try:
resp = await client.get(url, timeout=20, follow_redirects=True)
if resp.raise_for_status():
logger.debug(f"{url} download success!")
return resp.content
except httpx.HTTPError:
pass
logger.warning(f"{url} download failed!")
return None
# 下载资源列表
async with httpx.AsyncClient() as client:
try:
if content := await _download(client, "download_list.json"):
resource_list = json.loads(content.decode("utf-8"))
else:
logger.warning("download_list.json not found!")
return
except json.decoder.JSONDecodeError:
logger.warning("download_list.json is not a valid json file!")
return
# 检查资源目录下文件是否完整
drink_pic_path = Path(config.whatpic_res_path) / "drink_pic" # 本地资源目录
eat_pic_path = Path(config.whatpic_res_path) / "eat_pic" # 本地资源目录
me_pic_path = Path(config.whatpic_res_path) / "me_pic"
# TODO: 检查文件是否完整
# 创建下载任务列表
download_list: list[tuple[Path, str]] = []
# 添加下载任务
for item in resource_list["drink_pic"]:
path_name = (
"drink_pic/" + item["name"]
) # 在线资源路径: github.com/Cvandia/nonebot-plugin-whateat-pic/main/res/+ path_name
download_list.append((drink_pic_path / item["name"], path_name))
for item in resource_list["eat_pic"]:
path_name = "eat_pic/" + item["name"] # 在线资源路径
download_list.append((eat_pic_path / item["name"], path_name))
for item in resource_list["me_pic"]:
path_name = "me_pic/" + item["name"] # 在线资源路径
download_list.append((me_pic_path / item["name"], path_name))
if download_list:
logger.info("Downloading images ...")
else:
return
# 下载资源
async with httpx.AsyncClient() as client:
exist_count = 0
async def download_image(file_path: Path, file_name: str):
if file_path.exists():
logger.debug(f"{file_path} already exists, skipping download.")
nonlocal exist_count
exist_count += 1
return
if content := await _download(client, file_name):
file_path.parent.mkdir(parents=True, exist_ok=True)
with file_path.open("wb") as f:
f.write(content)
with Progress(
*Progress.get_default_columns(), "[yellow]{task.completed}/{task.total}"
) as progress:
progress_task = progress.add_task(
"[green]Downloading pic...", total=len(download_list)
)
tasks = [
download_image(file_path, file_name)
for file_path, file_name in download_list
]
for task in asyncio.as_completed(tasks):
await task
progress.update(progress_task, advance=1)
logger.info(f"Downloaded {len(download_list) - exist_count} images.")
driver = get_driver()
@driver.on_startup
async def on_startup():
logger.info("Checking resources...")
task = asyncio.create_task(check_resource())
await task
# 测试代码
@@ -0,0 +1,46 @@
import time
from nonebot.adapters import Event
from .config import config
cd = config.whateat_cd
max_count = config.whateat_max
def check_iscd(last_time: float) -> tuple[bool, float, float]:
"""
判断是否在冷却时间内
Args:
last_time(float): 上次使用时间
Returns:
- tuple[bool, float, float]: 是否在冷却时间内, 剩余冷却时间, 当前时间
"""
now_time = time.time()
if now_time - last_time < cd:
return True, cd - (now_time - last_time), last_time
return False, 0, now_time
def check_ismax(message: Event, user_count: dict) -> tuple[bool, dict]:
"""
判断是否达到最大次数
Args:
message(Event): 消息事件
user_count(dict): 用户使用次数记录
Returns:
- tuple[bool, dict]: 是否达到最大次数, 用户使用次数记录
"""
user_id = message.get_user_id()
if max_count == 0:
return False, {}
if user_id not in user_count:
user_count[f"{user_id}"] = 0
if user_count[f"{user_id}"] < max_count:
user_count[f"{user_id}"] += 1
return False, user_count
return True, user_count
+12
View File
@@ -0,0 +1,12 @@
import nonebot_plugin_localstore as store
from nonebot.plugin import get_plugin_config
from pydantic import BaseModel, Field
class Config(BaseModel):
whateat_cd: int = Field(default=10)
whateat_max: int = Field(default=0)
whatpic_res_path: str = Field(default=str(store.get_plugin_cache_dir()))
config = get_plugin_config(Config)
+33
View File
@@ -0,0 +1,33 @@
from pathlib import Path
from nonebot.log import logger
from .config import config
def save_pic(img: bytes, img_type: str, name: str) -> None:
"""
保存图片
"""
save_path = Path(config.whatpic_res_path) / f"{img_type}_pic" / (name + ".jpg")
if isinstance(img, bytes):
with Path.open(save_path, "wb") as f:
f.write(img)
else:
logger.error(f"img must be bytes, but got {type(img)}")
raise TypeError("img must be bytes")
def delete_pic(img_type: str, name: str) -> None:
"""
删除图片
"""
delete_path = Path(config.whatpic_res_path) / f"{img_type}_pic" / (name + ".jpg")
if delete_path.exists():
try:
delete_path.unlink()
except OSError as e:
logger.error(f"Error: {e}")
raise
else:
raise FileNotFoundError(f"{delete_path} not found")
+246
View File
@@ -0,0 +1,246 @@
import io
import secrets
import aiofiles
from nonebot import get_driver
from nonebot.adapters import Event
from nonebot.log import logger
from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna
from nonebot_plugin_alconna.uniseg import Image, Text, UniMessage
from nonebot_plugin_alconna.uniseg.tools import image_fetch
from nonebot_plugin_apscheduler import scheduler
from .check_user_data import check_iscd, check_ismax
from .files import delete_pic, save_pic
from .menu import Menu
from .random_pic import random_pic
NICKNAME = list(get_driver().config.nickname)
BOT_NAME = NICKNAME[0] if NICKNAME else "脑积水"
TIME = 0.0
USER_DATA = {}
MAX_MSG = [
"你今天吃的够多了!不许再吃了(´-ωก`)",
"吃吃吃,就知道吃,你都吃饱了!明天再来(▼皿▼#)",
"(*`へ´*)你猜我会不会再给你发好吃的图片",
f"没得吃的了,{BOT_NAME}的食物都被你这坏蛋吃光了!",
"你在等我给你发好吃的?做梦哦!你都吃那么多了,不许再吃了!ヽ(≧Д≦)ノ",
]
eat_pic_matcher = on_alconna(
Alconna("今天吃什么"),
use_cmd_start=True,
)
drink_pic_matcher = on_alconna(
Alconna("今天喝什么"),
use_cmd_start=True,
)
me_pic_matcher = on_alconna(
Alconna("今天蜜什么"),
use_cmd_start=True,
)
view_menu_matcher = on_alconna(
Alconna("全部菜单", Args["img_type?", str]),
use_cmd_start=True,
aliases=("查看菜单", "查看菜品"),
)
add_menu_matcher = on_alconna(
Alconna(
"添加菜单", Args["name?", str], Args["img_type?", str], Args["img?", Image]
),
use_cmd_start=True,
permission=SUPERUSER,
)
del_menu_matcher = on_alconna(
Alconna("删除菜单", Args["name?", str], Args["img_type?", str]),
use_cmd_start=True,
permission=SUPERUSER,
)
eat_pic_matcher.shortcut(
r"^[今|明|后]?[天|日]?(早|中|晚)?(上|午|餐|饭|夜宵|宵夜|早|晚)吃(什么|啥|点啥)$",
fuzzy=False,
prefix=True,
)
drink_pic_matcher.shortcut(
r"^[今|明|后]?[天|日]?(早|中|晚)?(上|午|餐|饭|夜宵|宵夜|早|晚)喝(什么|啥|点啥)$",
fuzzy=False,
prefix=True,
)
me_pic_matcher.shortcut(
r"^(我要蜜)?[今|明|后]?[天|日]?(早|中|晚)?(上|午|餐|饭|夜宵|宵夜|早|晚)蜜(什么|啥|点啥)$",
fuzzy=False,
prefix=True,
)
@eat_pic_matcher.handle()
async def handle_eat_pic(event: Event):
global TIME
global USER_DATA
check_max_result, USER_DATA = check_ismax(event, USER_DATA)
check_result, remain_time, TIME = check_iscd(TIME)
if check_max_result:
await UniMessage.text(secrets.choice(MAX_MSG)).finish()
elif check_result:
await UniMessage.text(f"cd冷却中,还有{remain_time:.2f}秒").finish()
else:
pic_path, pic_name = random_pic("eat")
send_msg: UniMessage = UniMessage(Text(f"🎉{BOT_NAME}建议你吃🎉\n{pic_name}"))
async with aiofiles.open(pic_path, "rb") as f:
send_msg.append(Image(raw=await f.read()))
await send_msg.finish()
@drink_pic_matcher.handle()
async def handle_drink_pic(event: Event):
global TIME
global USER_DATA
check_max_result, USER_DATA = check_ismax(event, USER_DATA)
check_result, remain_time, TIME = check_iscd(TIME)
if check_max_result:
await UniMessage.text(secrets.choice(MAX_MSG)).finish()
elif check_result:
await UniMessage.text(f"cd冷却中,还有{remain_time:.2f}秒").finish()
else:
pic_path, pic_name = random_pic("drink")
send_msg: UniMessage = UniMessage(Text(f"🎉{BOT_NAME}建议你喝🎉\n{pic_name}"))
async with aiofiles.open(pic_path, "rb") as f:
send_msg.append(Image(raw=await f.read()))
await send_msg.finish()
@me_pic_matcher.handle()
async def handle_me_pic(event: Event):
global TIME
global USER_DATA
check_max_result, USER_DATA = check_ismax(event, USER_DATA)
check_result, remain_time, TIME = check_iscd(TIME)
if check_max_result:
await UniMessage.text(secrets.choice(MAX_MSG)).finish()
elif check_result:
await UniMessage.text(f"cd冷却中,还有{remain_time:.2f}秒").finish()
else:
pic_path, pic_name = random_pic("me")
send_msg = UniMessage(Text(f"🎉{BOT_NAME}建议你喝🎉\n{pic_name}"))
send_msg.append(Image(path=pic_path))
await send_msg.finish()
@view_menu_matcher.handle()
async def handle_view_menu(img_type: Match[str]):
if img_type.available:
view_menu_matcher.set_path_arg("img_type", img_type.result)
@view_menu_matcher.got_path("img_type", prompt=f"请告诉{BOT_NAME}具体菜单类型吧")
async def _(img_type: str):
menu_type = img_type.strip()
if menu_type in ["菜单", "菜品"]:
menu_type = "eat"
elif menu_type in ["饮料", "饮品"]:
menu_type = "drink"
elif menu_type in ["蜜雪", "蜜", "蜜雪冰城"]:
menu_type = "me"
else:
await UniMessage.text("菜单类型错误,请重新输入").finish()
try:
menu = Menu(menu_type)
send_msg_list = UniMessage(Text("菜单如下:"))
for img in menu.draw_menu():
img_bytesio = io.BytesIO()
img.save(img_bytesio, format="JPEG")
send_msg_list.append(Image(raw=img_bytesio)) # type: ignore
await send_msg_list.finish()
except OSError as e:
await UniMessage.text(f"没有找到菜单,请稍后重试\n{e}").finish()
@add_menu_matcher.handle()
async def _(name: Match[str], img_type: Match[str]):
if name.available:
add_menu_matcher.set_path_arg("name", name.result)
if img_type.available:
add_menu_matcher.set_path_arg("img_type", img_type.result)
@add_menu_matcher.got_path("name", prompt=f"请告诉{BOT_NAME}具体菜名或者饮品名吧")
async def _(name: str):
if not name:
await UniMessage.text("菜名不能为空,请重新输入").finish()
@add_menu_matcher.got_path("img_type", prompt=f"请告诉{BOT_NAME}具体菜单类型吧")
async def _(img_type: str):
if img_type in ["菜品", "菜单"]:
add_menu_matcher.set_path_arg("img_type", "eat")
elif img_type in ["饮料", "饮品"]:
add_menu_matcher.set_path_arg("img_type", "drink")
elif img_type in ["蜜雪", "蜜", "蜜雪冰城"]:
add_menu_matcher.set_path_arg("img_type", "me")
else:
await UniMessage.text("菜单类型错误,请重新输入").finish()
@add_menu_matcher.got_path(
"img", prompt=f"请告诉{BOT_NAME}图片吧", middleware=image_fetch
)
async def _(
name: str,
img_type: str,
img: bytes,
):
if not img:
await UniMessage.text("图片不能为空,请重新输入").finish()
try:
save_pic(img, img_type=img_type, name=name)
await UniMessage.text(f"成功添加{name}").finish()
except OSError as e:
await UniMessage.text(f"添加失败,请稍后重试\n{e}").finish()
@del_menu_matcher.handle()
async def _(name: Match[str], img_type: Match[str]):
if name.available:
del_menu_matcher.set_path_arg("name", name.result)
if img_type.available:
del_menu_matcher.set_path_arg("img_type", img_type.result)
@del_menu_matcher.got_path("name", prompt=f"请告诉{BOT_NAME}具体菜名或者饮品名吧")
async def _(name: str):
if not name:
await UniMessage.text("菜名不能为空,请重新输入").finish()
@del_menu_matcher.got_path("img_type", prompt=f"请告诉{BOT_NAME}具体菜单类型吧")
async def _(img_type: str, name: str):
if img_type in ["菜品", "菜单"]:
img_type = "eat"
elif img_type in ["饮料", "饮品"]:
img_type = "drink"
elif img_type in ["蜜雪", "蜜", "蜜雪冰城"]:
img_type = "me"
else:
await UniMessage.text("菜单类型错误,请重新输入").finish()
try:
delete_pic(img_type, name) # type: ignore
await UniMessage.text(f"成功删除{name}").finish()
except FileNotFoundError as e:
await UniMessage.text(f"删除失败, {e}").finish()
# 每日8点清空用户数据
@scheduler.scheduled_job("cron", hour=8)
async def _():
USER_DATA.clear()
logger.info("已清空用户数据")
+69
View File
@@ -0,0 +1,69 @@
from collections.abc import Iterator
from pathlib import Path
from typing import Literal
from PIL import Image, ImageDraw, ImageFont
from .config import config
class Menu:
"""
生成菜单的类
"""
dish_path: Path # 菜品图片的路径
menu_background: Image.Image # 菜单背景图
menu_font: ImageFont.FreeTypeFont # 菜单字体
def __init__(self, menu_type: Literal["drink", "eat", "me"]) -> None:
# 菜单图片的路径
self.dish_path = Path(config.whatpic_res_path) / f"{menu_type}_pic"
# 获取所有菜品的名字
self.all_dish_name = [i.stem for i in self.dish_path.iterdir()]
# 菜单背景图
self.menu_background = Image.open(
Path(__file__).parent / "menu_res" / "menu_bg.jpg"
)
# 字体大小
self.font_size = 30
# 加载字体
self.menu_font = ImageFont.truetype(
str(Path(__file__).parent / "menu_res" / "FZSJ-QINGCRJ.TTF"), self.font_size
)
@property
def menu_bg_size(self) -> tuple:
"""
获取菜单背景图的大小
"""
return self.menu_background.size
# 生成迭代器用于for循环遍历所有的生成的菜单
def draw_menu(self) -> Iterator[Image.Image]:
# 每个菜单的行数: 背景图高度 - 200(上下边距) // (字体大小+10(行间距)) = 菜单行数
line_num = (self.menu_bg_size[1] - 150) // (self.font_size + 10)
# 总共需要合成的图片数量
img_num = len(self.all_dish_name) // line_num + 1
# 生成所有菜单的图片
for i in range(img_num):
# 生成一张背景图的副本
menu_img = self.menu_background.copy()
# 生成draw对象
draw = ImageDraw.Draw(menu_img)
# 生成菜单的名字
for j in range(line_num):
# 如果菜单名字已经全部生成完毕
if i * line_num + j >= len(self.all_dish_name):
break
# 生成菜单序号+名字,(居中显示)
draw.text(
((self.menu_bg_size[0] - 300) // 2, 75 + j * (self.font_size + 10)),
f"{i * line_num + j + 1}.{self.all_dish_name[i * line_num + j]}",
font=self.menu_font,
fill="black",
)
yield menu_img
# 测试程序
Binary file not shown.
Generated
+1293
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
[tool.pdm]
distribution = true
[tool.pdm.build]
includes = ["nonebot_plugin_whateat_pic"]
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
name = "nonebot-plugin-whateat-pic"
version = "1.5.9"
description = "基于Nonebot2的今天吃什么(离线版)"
authors = [
{ name = "Cvandia", email = "[email protected]" },
{ name = "Cvandia" },
]
dependencies = [
"nonebot2>=2.0.0",
"httpx>=0.19.0",
"pillow>=10.4.0",
"nonebot-plugin-alconna<1.0.0,>=0.53.1",
"rich<14.0.0,>=13.9.4",
"nonebot-plugin-apscheduler>=0.5.0",
"nonebot-plugin-localstore>=0.7.4",
"aiofiles>=0.7.0",
]
requires-python = "<4,>=3.9"
readme = "README.md"
license = { text = "MIT" }
[project.urls]
homepage = "https://github.com/Cvandia/nonebot-plugin-whateat-pic"
repository = "https://github.com/Cvandia/nonebot-plugin-whateat-pic"
[dependency-groups]
dev = [
"black<23.0.0,>=22.1.0",
"ruff>=0.8.1",
"pre-commit>=4.0.1",
]
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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