🤡我知道您急,您先别急,这是有bug的,我还没改完,我就想换台设备来写

This commit is contained in:
EillesWan 2024-07-20 22:43:54 +08:00
parent d0d347e4e3
commit fe8f6bd443
120 changed files with 4965 additions and 3069 deletions

4
.gitignore vendored
View File

@ -18,6 +18,10 @@ config.yml
config.example.yml
compile.bat
src/resources/templates/latest-debug.html
src/plugins/trimo_plugin_msctconverter/config
src/plugins/trimo_plugin_msctconverter/temp
# vuepress
.github
pyproject.toml

View File

@ -42,7 +42,7 @@
版权所有 © 2024 SnowyKami & EillesWan
轻雪机器人睿乐定制版(LiteyukiBot-TriM)根据 第一版 汉钰律许可协议(“本协议”)授权。\
轻雪机器人睿乐定制版(LiteyukiBot-TriMO)根据 第一版 汉钰律许可协议(“本协议”)授权。\
任何人皆可从以下地址获得本协议副本:[汉钰律许可协议 第一版](https://gitee.com/EillesWan/YulvLicenses/raw/master/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.MD)。\
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
详细的准许和限制条款请见原协议文本。
@ -57,4 +57,4 @@
[lightyuki-link]: /
[python-link]: https://www.python.org/
[usage-link]: https://bot.liteyuki.icu/
[banner]: https://socialify.git.ci/TriM-Organization/LiteyukiBot-TriM/image?description=1&forks=1&issues=1&Plus&pulls=1&stargazers=1&theme=Auto&logo=https://gitee.com/TriM-Organization/Linglun-Converter/raw/master/resources/TriMO_LOGO.png
[banner]: https://socialify.git.ci/TriM-Organization/LiteyukiBot-TriM/image?description=1&forks=1&issues=1&Plus&pulls=1&stargazers=1&theme=Auto&logo=https://raw.githubusercontent.com/TriM-Organization/Linglun-Converter/master/resources/TriMO_LOGO.png

View File

@ -4,8 +4,8 @@ from git import Repo
from src.utils.base.config import get_config
remote_urls = [
"https://gitee.com/TriM-Organization/LiteyukiBot-TriM.git",
"https://github.com/TriM-Organization/LiteyukiBot-TriM.git"
"https://gitee.com/TriM-Organization/LiteyukiBot-TriMO.git",
"https://github.com/TriM-Organization/LiteyukiBot-TriMO.git"
]

View File

@ -23,12 +23,22 @@ from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar
from nonebot_plugin_alconna import (
UniMessage,
on_alconna,
Alconna,
Args,
Subcommand,
Arparma,
MultiVar,
)
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False)
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get(
"markdown_image", False
)
@on_alconna(
@ -36,42 +46,38 @@ markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).con
"ryounecho",
Args["text", str, ""],
),
permission=SUPERUSER
permission=SUPERUSER,
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if result.main_args.get("text"):
await matcher.finish(Message(unescape(result.main_args.get("text"))))
await matcher.finish(Message(unescape(result.main_args.get("text")))) # type: ignore
else:
await matcher.finish(f"君安!灵温向你问好~\n此机 {bot.self_id}")
@on_alconna(
command=Alconna(
"liteecho",
Args["text", str, ""],
# Args["text", str, ""],
),
permission=SUPERUSER
# permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if result.main_args.get("text"):
await matcher.finish(Message(unescape(result.main_args.get("text"))))
else:
await matcher.finish(f"Hello! TriM-Liteyuki!\nRyBot {bot.self_id}")
await matcher.finish(f"Hello! TriMO-Liteyuki!\nRyBot {bot.self_id}")
@on_alconna(
aliases={"更新灵温"},
command=Alconna(
"update-ryoun"
),
permission=SUPERUSER
aliases={"更新灵温"}, command=Alconna("update-ryoun"), permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, event: T_MessageEvent):
# 使用git pull更新
ulang = get_user_lang(str(event.user.id if isinstance(event, satori.event.Event) else event.user_id))
ulang = get_user_lang(
str(event.user.id if isinstance(event, satori.event.Event) else event.user_id)
)
success, logs = update_liteyuki()
reply = "尹灵温 更新完成!\n"
reply += f"```\n{logs}\n```\n"
@ -82,11 +88,11 @@ async def _(bot: T_Bot, event: T_MessageEvent):
@on_alconna(
aliases={"重启灵温","重启尹灵温", "重载灵温"},
aliases={"重启灵温", "重启尹灵温", "重载灵温"},
command=Alconna(
"reload-ryoun",
),
permission=SUPERUSER
permission=SUPERUSER,
).handle()
# Satori OK
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
@ -95,13 +101,16 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
temp_data.data.update(
{
"reload" : True,
"reload_time" : time.time(),
"reload_bot_id" : bot.self_id,
"reload_session_type": event_utils.get_message_type(event),
"reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id) if not isinstance(event,
satori.event.Event) else event.channel.id,
"delta_time" : 0
"reload": True,
"reload_time": time.time(),
"reload_bot_id": bot.self_id,
"reload_session_type": event_utils.get_message_type(event),
"reload_session_id": (
(event.group_id if event.message_type == "group" else event.user_id)
if not isinstance(event, satori.event.Event)
else event.channel.id
),
"delta_time": 0,
}
)
@ -117,34 +126,31 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
"set",
Args["key", str]["value", Any],
alias=["设置"],
),
Subcommand(
"get",
Args["key", str, None],
alias=["查询", "获取"]
),
Subcommand(
"remove",
Args["key", str],
alias=["删除"]
)
Subcommand("get", Args["key", str, None], alias=["查询", "获取"]),
Subcommand("remove", Args["key", str], alias=["删除"]),
),
permission=SUPERUSER
permission=SUPERUSER,
).handle()
# Satori OK
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
stored_config: StoredConfig = common_db.where_one(
StoredConfig(), default=StoredConfig()
)
if result.subcommands.get("set"):
key, value = result.subcommands.get("set").args.get("key"), result.subcommands.get("set").args.get("value")
key, value = result.subcommands.get("set").args.get(
"key"
), result.subcommands.get("set").args.get("value")
try:
value = eval(value)
except:
pass
stored_config.config[key] = value
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}")
await matcher.finish(
f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}"
)
elif result.subcommands.get("get"):
key = result.subcommands.get("get").args.get("key")
file_config = load_from_yaml("config.yml")
@ -168,29 +174,36 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher
if key in stored_config.config:
stored_config.config.pop(key)
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
await matcher.finish(
f"{ulang.get('liteyuki.config_remove_success', KEY=key)}"
)
else:
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
@on_alconna(
aliases={"切换图片模式"},
command=Alconna(
"switch-image-mode"
),
permission=SUPERUSER
aliases={"切换图片模式"}, command=Alconna("switch-image-mode"), permission=SUPERUSER
).handle()
# Satori OK
async def _(event: T_MessageEvent, matcher: Matcher):
global markdown_image
# 切换图片模式False以图片形式发送True以markdown形式发送
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
stored_config.config["markdown_image"] = not stored_config.config.get("markdown_image", False)
stored_config: StoredConfig = common_db.where_one(
StoredConfig(), default=StoredConfig()
)
stored_config.config["markdown_image"] = not stored_config.config.get(
"markdown_image", False
)
markdown_image = stored_config.config["markdown_image"]
common_db.save(stored_config)
await matcher.finish(
ulang.get("liteyuki.image_mode_on" if stored_config.config["markdown_image"] else "liteyuki.image_mode_off"))
ulang.get(
"liteyuki.image_mode_on"
if stored_config.config["markdown_image"]
else "liteyuki.image_mode_off"
)
)
# @on_alconna(
@ -209,7 +222,7 @@ async def _(event: T_MessageEvent, matcher: Matcher):
"/function",
Args["function", str]["args", MultiVar(str), ()],
),
permission=SUPERUSER
permission=SUPERUSER,
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
@ -226,9 +239,9 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
args: tuple[str] = result.main_args.get("args", ())
_args = []
_kwargs = {
"USER_ID" : str(event.user_id),
"GROUP_ID": str(event.group_id) if event.message_type == "group" else "0",
"BOT_ID" : str(bot.self_id)
"USER_ID": str(event.user_id),
"GROUP_ID": str(event.group_id) if event.message_type == "group" else "0",
"BOT_ID": str(bot.self_id),
}
for arg in args:
@ -256,7 +269,7 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
"/api",
Args["api", str]["args", MultiVar(AnyStr), ()],
),
permission=SUPERUSER
permission=SUPERUSER,
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
@ -270,7 +283,9 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
"""
api_name = result.main_args.get("api")
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数但每个参数间用空格分隔空格是%20
args: tuple[str] = result.main_args.get(
"args", ()
) # 类似于url参数但每个参数间用空格分隔空格是%20
args_dict = {}
for arg in args:
@ -280,7 +295,11 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
if api_name in need_user_id and "user_id" not in args_dict:
args_dict["user_id"] = str(event.user_id)
if api_name in need_group_id and "group_id" not in args_dict and event.message_type == "group":
if (
api_name in need_group_id
and "group_id" not in args_dict
and event.message_type == "group"
):
args_dict["group_id"] = str(event.group_id)
if "message" in args_dict:
@ -303,29 +322,53 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
@Bot.on_calling_api # 图片模式检测
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
# 截获大图发送转换为markdown发送
if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get(
"user_id") != bot.self_id:
if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg":
if (
api in ["send_msg", "send_private_msg", "send_group_msg"]
and markdown_image
and data.get("user_id") != bot.self_id
):
if (
api == "send_msg"
and data.get("message_type") == "private"
or api == "send_private_msg"
):
session_type = "private"
session_id = data.get("user_id")
elif api == "send_msg" and data.get("message_type") == "group" or api == "send_group_msg":
elif (
api == "send_msg"
and data.get("message_type") == "group"
or api == "send_group_msg"
):
session_type = "group"
session_id = data.get("group_id")
else:
return
if len(data.get("message", [])) == 1 and data["message"][0].get("type") == "image":
if (
len(data.get("message", [])) == 1
and data["message"][0].get("type") == "image"
):
file: str = data["message"][0].data.get("file")
# file:// http:// base64://
if file.startswith("http"):
result = await md.send_md(await md.image_async(file), bot, message_type=session_type,
session_id=session_id)
result = await md.send_md(
await md.image_async(file),
bot,
message_type=session_type,
session_id=session_id,
)
elif file.startswith("file"):
file = file.replace("file://", "")
result = await md.send_image(open(file, "rb").read(), bot, message_type=session_type,
session_id=session_id)
result = await md.send_image(
open(file, "rb").read(),
bot,
message_type=session_type,
session_id=session_id,
)
elif file.startswith("base64"):
file_bytes = base64.b64decode(file.replace("base64://", ""))
result = await md.send_image(file_bytes, bot, message_type=session_type, session_id=session_id)
result = await md.send_image(
file_bytes, bot, message_type=session_type, session_id=session_id
)
else:
return
raise MockApiException(result=result)
@ -364,7 +407,7 @@ async def _(bot: T_Bot):
if isinstance(bot, satori.Bot):
await bot.send_message(
channel_id=reload_session_id,
message="灵温 重载耗时 %.2f" % delta_time
message="灵温 重载耗时 %.2f" % delta_time,
)
else:
await bot.call_api(
@ -372,7 +415,7 @@ async def _(bot: T_Bot):
message_type=reload_session_type,
user_id=reload_session_id,
group_id=reload_session_id,
message="灵温 重载耗时 %.2f" % delta_time
message="灵温 重载耗时 %.2f" % delta_time,
)
@ -392,22 +435,21 @@ async def every_day_update():
# 需要用户id的api
need_user_id = (
"send_private_msg",
"send_msg",
"set_group_card",
"set_group_special_title",
"get_stranger_info",
"get_group_member_info"
"send_private_msg",
"send_msg",
"set_group_card",
"set_group_special_title",
"get_stranger_info",
"get_group_member_info",
)
need_group_id = (
"send_group_msg",
"send_msg",
"set_group_card",
"set_group_name",
"set_group_special_title",
"get_group_member_info",
"get_group_member_list",
"get_group_honor_info"
)
"send_group_msg",
"send_msg",
"set_group_card",
"set_group_name",
"set_group_special_title",
"get_group_member_info",
"get_group_member_list",
"get_group_honor_info",
)

View File

@ -183,4 +183,7 @@ async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher
if send_as_md:
await md.send_md(reply, bot, event=event)
else:
await matcher.finish(reply)
if reply:
await matcher.finish(reply)
else:
await matcher.finish("其实还没做help\t——神羽")

View File

@ -18,6 +18,7 @@ from nonebot_plugin_alconna import (
# AlconnaQuery,
# Query,
Arparma,
Args,
)
require("nonebot_plugin_apscheduler")
@ -59,12 +60,18 @@ yanlun = on_alconna(
action=store_true,
),
Option("-c|--count", default=False, alias={"统计"}, action=store_true),
Option("-l|--length", default=1.0, args=Args["length", float | int, 1.0]),
),
)
# 每天4点更新
@scheduler.scheduled_job("cron", hour=4)
yanlun_path = (
"https://gitee.com/TriM-Organization/LinglunStudio/raw/master/resources/myWords.txt"
)
# 每天1点更新
@scheduler.scheduled_job("cron", hour=1)
async def every_day_update():
ulang = Language(get_default_lang_code(), "zh-WY")
nonebot.logger.success(ulang.get("yanlun.refresh.success", COUNT=update_yanlun()))
@ -79,15 +86,17 @@ def update_yanlun():
lunar_date = (lunar_datetime.lunar_month, lunar_datetime.lunar_day)
if solar_date == (4, 3):
yanlun_texts = ["金羿ELS 生日快乐~"]
yanlun_texts = ["金羿ELS 生日快乐~", "Happy Birthday, Eilles!"]
elif solar_date == (8, 6):
yanlun_texts = ["诸葛八卦 生日快乐~"]
yanlun_texts = ["诸葛亮与八卦阵 生日快乐~", "Happy Birthday, bgArray~!"]
elif solar_date == (8, 16):
yanlun_texts = ["鱼旧梦 生日快乐~", "Happy Birthday, ElapsingDreams~!"]
else:
try:
yanlun_texts = (
requests.get(
"https://gitee.com/TriM-Organization/LinglunStudio/raw/master/resources/myWords.txt",
yanlun_path,
)
.text.strip("\n")
.split("\n")
@ -165,13 +174,13 @@ async def _(
# count: Query[bool] = AlconnaQuery("count.value", False),
):
# print(result.options)
ulang = get_user_lang(event_utils.get_user_id(event)) # type: ignore
if result.options["refresh"].value:
ulang = get_user_lang(event_utils.get_user_id(event)) # type: ignore
global yanlun_texts
try:
yanlun_texts = (
requests.get(
"https://gitee.com/TriM-Organization/LinglunStudio/raw/master/resources/myWords.txt",
yanlun_path,
)
.text.strip("\n")
.split("\n")
@ -205,7 +214,6 @@ async def _(
)
yanlun_texts = ["灵光焕发 深艺献心"]
if result.options["count"].value:
ulang = get_user_lang(event_utils.get_user_id(event)) # type: ignore
authors = [
(
("B站")
@ -250,4 +258,27 @@ async def _(
+ ulang.get("yanlun.count.tail", NUM=total)
)
)
await yanlun.finish(UniMessage.text(random.choice(yanlun_texts)))
final_length = 0
try:
final_length += result.options["length"].args["length"]
except:
final_length = 1
(
(
await yanlun.finish(
UniMessage.text(
"\n".join([random.choice(yanlun_texts) for i in range(iill)])
if iill <= 100
else ulang.get("yanlun.length.toolong")
)
)
if iill > 0
else await yanlun.finish(
UniMessage.text(ulang.get("yanlun.length.tooshort"))
)
)
if (iill := int(final_length)) == final_length
else await yanlun.finish(UniMessage.text(ulang.get("yanlun.length.float")))
)

View File

@ -1,42 +0,0 @@
import nonebot
from nonebot import require
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import (
on_alconna,
Alconna,
Subcommand,
UniMessage,
Option,
store_true,
Arparma,
)
musicreater_convert = on_alconna(
aliases={"musicreater_convert","音乐转换","midi转换"},
command=Alconna(
"msctcvt",
Option(
"-r|--refresh",
default=False,
alias={"refr", "r", "刷新"},
action=store_true,
),
Subcommand(
"memory",
alias={"mem", "m", "内存"},
),
Subcommand(
"process",
alias={"proc", "p", "进程"},
),
# Subcommand(
# "refresh",
# alias={"refr", "r", "刷新"},
# ),
),
)

View File

@ -0,0 +1,13 @@
## 版权声明
本插件由 汉钰律许可协议 授权开源,兼容并继承自 MIT 许可协议。
Copyright (c) 2022 MeetWq
版权所有 © 2024 EillesWan & MeetWq
猜成语-睿乐特别版(trimo_plugin_handle)根据 第一版 汉钰律许可协议(“本协议”)授权。\
任何人皆可从以下地址获得本协议副本:[汉钰律许可协议 第一版](https://gitee.com/EillesWan/YulvLicenses/raw/master/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.MD)。\
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
详细的准许和限制条款请见原协议文本。

View File

@ -0,0 +1,268 @@
import asyncio
from asyncio import TimerHandle
from typing import Any, Dict
from nonebot import on_regex, require, on_command
from nonebot.matcher import Matcher
from nonebot.params import RegexDict
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
from nonebot.rule import to_me
from nonebot.utils import run_sync
from nonebot.permission import SUPERUSER
from typing_extensions import Annotated
require("nonebot_plugin_alconna")
require("nonebot_plugin_session")
from nonebot_plugin_alconna import (
Alconna,
AlconnaQuery,
Image,
Option,
Query,
Text,
UniMessage,
on_alconna,
store_true,
Args,
Arparma,
)
from nonebot_plugin_session import SessionId, SessionIdType
from .config import Config, handle_config
from .data_source import GuessResult, Handle
from .utils import random_idiom
__plugin_meta__ = PluginMetadata(
name="猜成语",
description="猜成语-睿乐特别版",
usage=(
"@我 + “猜成语”开始游戏;\n"
"你有十次的机会猜一个四字词语;\n"
"每次猜测后,汉字与拼音的颜色将会标识其与正确答案的区别;\n"
"青色 表示其出现在答案中且在正确的位置;\n"
"橙色 表示其出现在答案中但不在正确的位置;\n"
"每个格子的 汉字、声母、韵母、声调 都会独立进行颜色的指示。\n"
"当四个格子都为青色时,你便赢得了游戏!\n"
"可发送“结束”结束游戏;可发送“提示”查看提示。\n"
"使用 --strict 选项开启非默认的成语检查,即猜测的短语必须是成语,\n"
"如:@我 猜成语 --strict"
),
type="application",
homepage="https://github.com/noneplugin/nonebot-plugin-handle",
config=Config,
supported_adapters=inherit_supported_adapters(
"nonebot_plugin_alconna", "nonebot_plugin_session"
),
extra={
"example": "@小Q 猜成语",
},
)
games: Dict[str, Handle] = {}
timers: Dict[str, TimerHandle] = {}
UserId = Annotated[str, SessionId(SessionIdType.GROUP)]
def game_is_running(user_id: UserId) -> bool:
return user_id in games
def game_not_running(user_id: UserId) -> bool:
return user_id not in games
handle = on_alconna(
Alconna("handle", Option("-s|--strict", default=False, action=store_true)),
aliases=("猜成语",),
rule=to_me() & game_not_running,
use_cmd_start=True,
block=True,
priority=13,
)
handle_hint = on_alconna(
"提示",
rule=game_is_running,
use_cmd_start=True,
block=True,
priority=13,
)
handle_stop = on_alconna(
"结束",
aliases=("结束游戏", "结束猜成语"),
rule=game_is_running,
use_cmd_start=True,
block=True,
priority=13,
)
handle_answer = on_alconna(
Alconna(
"答案",
Option(
"-g|--group",
default="Now",
args=Args["group", str, "Now"],
),
Option(
"-l|--list",
default=False,
action=store_true,
),
),
# rule=game_is_running,
use_cmd_start=True,
permission=SUPERUSER,
block=True,
priority=13,
)
# handle_update = on_alconna(
# "更新词库",
# aliases=("刷新词库", "猜成语刷新词库"),
# rule=to_me(),
# use_cmd_start=True,
# block=True,
# priority=13,
# )
handle_idiom = on_regex(
r"^(?P<idiom>[\u4e00-\u9fa5]{4})$",
rule=game_is_running,
block=True,
priority=14,
)
def stop_game(user_id: str):
if timer := timers.pop(user_id, None):
timer.cancel()
games.pop(user_id, None)
async def stop_game_timeout(matcher: Matcher, user_id: str):
game = games.get(user_id, None)
stop_game(user_id)
if game:
msg = "猜成语超时,游戏结束。"
if len(game.guessed_idiom) >= 1:
msg += f"\n{game.result}"
await matcher.finish(msg)
def set_timeout(matcher: Matcher, user_id: str, timeout: float = 300):
if timer := timers.get(user_id, None):
timer.cancel()
loop = asyncio.get_running_loop()
timer = loop.call_later(
timeout, lambda: asyncio.ensure_future(stop_game_timeout(matcher, user_id))
)
timers[user_id] = timer
@handle.handle()
async def _(
matcher: Matcher,
user_id: UserId,
strict: Query[bool] = AlconnaQuery("strict.value", False),
):
is_strict = handle_config.handle_strict_mode or strict.result
idiom, explanation = random_idiom()
game = Handle(idiom, explanation, strict=is_strict)
games[user_id] = game
set_timeout(matcher, user_id)
msg = Text(
f"你有{game.times}次机会猜一个四字成语,"
+ ("发送有效成语以参与游戏。" if is_strict else "发送任意四字词语以参与游戏。")
) + Image(raw=await run_sync(game.draw)())
await msg.send()
@handle_hint.handle()
async def _(matcher: Matcher, user_id: UserId):
game = games[user_id]
set_timeout(matcher, user_id)
await UniMessage.image(raw=await run_sync(game.draw_hint)()).send()
@handle_stop.handle()
async def _(matcher: Matcher, user_id: UserId):
game = games[user_id]
stop_game(user_id)
msg = "游戏已结束"
if len(game.guessed_idiom) >= 1:
msg += f"\n{game.result}"
await matcher.finish(msg)
# @handle_update.handle()
@handle_idiom.handle()
async def _(matcher: Matcher, user_id: UserId, matched: Dict[str, Any] = RegexDict()):
game = games[user_id]
set_timeout(matcher, user_id)
idiom = str(matched["idiom"])
result = game.guess(idiom)
if result in [GuessResult.WIN, GuessResult.LOSS]:
stop_game(user_id)
msg = Text(
(
"恭喜你猜出了成语!"
if result == GuessResult.WIN
else "很遗憾,没有人猜出来呢"
)
+ f"\n{game.result}"
) + Image(raw=await run_sync(game.draw)())
await msg.send()
elif result == GuessResult.DUPLICATE:
await matcher.finish("你已经猜过这个成语了呢")
elif result == GuessResult.ILLEGAL:
await matcher.finish(f"你确定“{idiom}”是个成语吗?")
else:
await UniMessage.image(raw=await run_sync(game.draw)()).send()
@handle_answer.handle()
async def _(
result: Arparma,
matcher: Matcher,
user_id: UserId,
):
if result.options["list"].value:
await handle_answer.finish(
UniMessage.text(
"\n".join("{}-{}".format(i, j.idiom) for i, j in games.items())
)
)
return
try:
if result.options["group"].args["group"] == "Now":
session_numstr = user_id
else:
session_numstr = "qq_OneBot V11_2378756507_{}".format(
result.options["group"].args["group"]
)
except:
session_numstr = user_id
if session_numstr in games.keys():
await handle_answer.finish(UniMessage.text(games[session_numstr].idiom))
else:
await handle_answer.finish(
UniMessage.text("{} 不存在开局的游戏".format(session_numstr))
)

View File

@ -0,0 +1,10 @@
from nonebot import get_plugin_config
from pydantic import BaseModel
class Config(BaseModel):
handle_strict_mode: bool = False
handle_color_enhance: bool = False
handle_config = get_plugin_config(Config)

View File

@ -0,0 +1,284 @@
from dataclasses import dataclass
from enum import Enum
from io import BytesIO
from typing import List, Optional, Tuple
from PIL import Image, ImageDraw
from PIL.Image import Image as IMG
from .config import handle_config
from .utils import get_pinyin, legal_idiom, load_font, save_jpg
class GuessResult(Enum):
WIN = 0 # 猜出正确成语
LOSS = 1 # 达到最大可猜次数,未猜出正确成语
DUPLICATE = 2 # 成语重复
ILLEGAL = 3 # 成语不合法
class GuessState(Enum):
CORRECT = 0 # 存在且位置正确
EXIST = 1 # 存在但位置不正确
WRONG = 2 # 不存在
@dataclass
class ColorGroup:
bg_color: str # 背景颜色
block_color: str # 方块颜色
correct_color: str # 存在且位置正确时的颜色
exist_color: str # 存在但位置不正确时的颜色
wrong_color_pinyin: str # 不存在时的颜色
wrong_color_char: str # 不存在时的颜色
NORMAL_COLOR = ColorGroup(
"#ffffff", "#f7f8f9", "#1d9c9c", "#de7525", "#b4b8be", "#5d6673"
)
ENHANCED_COLOR = ColorGroup(
"#ffffff", "#f7f8f9", "#5ba554", "#ff46ff", "#b4b8be", "#5d6673"
)
class Handle:
def __init__(self, idiom: str, explanation: str, strict: bool = False):
self.idiom: str = idiom # 成语
self.explanation: str = explanation # 释义
self.strict: bool = strict # 是否判断输入词语为成语
self.result = f"【成语】:{idiom}\n【释义】:{explanation}"
self.pinyin: List[Tuple[str, str, str]] = get_pinyin(idiom) # 拼音
self.length = 4
self.times: int = 10 # 可猜次数
self.guessed_idiom: List[str] = [] # 记录已猜成语
self.guessed_pinyin: List[List[Tuple[str, str, str]]] = [] # 记录已猜成语的拼音
self.block_size = (160, 160) # 文字块尺寸
self.block_padding = (20, 20) # 文字块之间间距
self.padding = (40, 40) # 边界间距
font_size_char = 60 # 汉字字体大小
font_size_pinyin = 30 # 拼音字体大小
font_size_tone = 22 # 声调字体大小
self.font_char = load_font("NotoSerifSC-Regular.otf", font_size_char)
self.font_pinyin = load_font("NotoSansMono-Regular.ttf", font_size_pinyin)
self.font_tone = load_font("NotoSansMono-Regular.ttf", font_size_tone)
self.colors = (
ENHANCED_COLOR if handle_config.handle_color_enhance else NORMAL_COLOR
)
def guess(self, idiom: str) -> Optional[GuessResult]:
if self.strict and not legal_idiom(idiom):
return GuessResult.ILLEGAL
if idiom in self.guessed_idiom:
return GuessResult.DUPLICATE
self.guessed_idiom.append(idiom)
self.guessed_pinyin.append(get_pinyin(idiom))
if idiom == self.idiom:
return GuessResult.WIN
if len(self.guessed_idiom) == self.times:
return GuessResult.LOSS
def draw_block(
self,
block_color: str,
char: str = "",
char_color: str = "",
initial: str = "",
initial_color: str = "",
final: str = "",
final_color: str = "",
tone: str = "",
tone_color: str = "",
underline: bool = False,
underline_color: str = "",
) -> IMG:
block = Image.new("RGB", self.block_size, block_color)
if not char:
return block
draw = ImageDraw.Draw(block)
char_size = self.font_char.getbbox(char)[2:]
x = (self.block_size[0] - char_size[0]) / 2
y = (self.block_size[1] - char_size[1]) / 5 * 3
draw.text((x, y), char, font=self.font_char, fill=char_color)
space = 5
need_space = bool(initial and final)
py_length = self.font_pinyin.getlength(initial + final)
if need_space:
py_length += space
py_start = (self.block_size[0] - py_length) / 2
x = py_start
y = self.block_size[0] / 8
draw.text((x, y), initial, font=self.font_pinyin, fill=initial_color)
x += self.font_pinyin.getlength(initial)
if need_space:
x += space
draw.text((x, y), final, font=self.font_pinyin, fill=final_color)
tone_size = self.font_tone.getbbox(tone)[2:]
x = (self.block_size[0] + py_length) / 2 + tone_size[0] / 3
y -= tone_size[1] / 3
draw.text((x, y), tone, font=self.font_tone, fill=tone_color)
if underline:
x = py_start
py_size = self.font_pinyin.getbbox(initial + final)[2:]
y = self.block_size[0] / 8 + py_size[1] + 2
draw.line((x, y, x + py_length, y), fill=underline_color, width=1)
y += 3
draw.line((x, y, x + py_length, y), fill=underline_color, width=1)
return block
def draw(self) -> BytesIO:
rows = min(len(self.guessed_idiom) + 1, self.times)
board_w = self.length * self.block_size[0]
board_w += (self.length - 1) * self.block_padding[0] + 2 * self.padding[0]
board_h = rows * self.block_size[1]
board_h += (rows - 1) * self.block_padding[1] + 2 * self.padding[1]
board_size = (board_w, board_h)
board = Image.new("RGB", board_size, self.colors.bg_color)
def get_states(guessed: List[str], answer: List[str]) -> List[GuessState]:
states = []
incorrect = []
for i in range(self.length):
if guessed[i] != answer[i]:
incorrect.append(answer[i])
else:
incorrect.append("_")
for i in range(self.length):
if guessed[i] == answer[i]:
states.append(GuessState.CORRECT)
elif guessed[i] in incorrect:
states.append(GuessState.EXIST)
incorrect[incorrect.index(guessed[i])] = "_"
else:
states.append(GuessState.WRONG)
return states
def get_pinyin_color(state: GuessState) -> str:
if state == GuessState.CORRECT:
return self.colors.correct_color
elif state == GuessState.EXIST:
return self.colors.exist_color
else:
return self.colors.wrong_color_pinyin
def get_char_color(state: GuessState) -> str:
if state == GuessState.CORRECT:
return self.colors.correct_color
elif state == GuessState.EXIST:
return self.colors.exist_color
else:
return self.colors.wrong_color_char
def block_pos(row: int, col: int) -> Tuple[int, int]:
x = self.padding[0] + (self.block_size[0] + self.block_padding[0]) * col
y = self.padding[1] + (self.block_size[1] + self.block_padding[1]) * row
return x, y
for i in range(len(self.guessed_idiom)):
idiom = self.guessed_idiom[i]
pinyin = self.guessed_pinyin[i]
char_states = get_states(list(idiom), list(self.idiom))
initial_states = get_states(
[p[0] for p in pinyin], [p[0] for p in self.pinyin]
)
final_states = get_states(
[p[1] for p in pinyin], [p[1] for p in self.pinyin]
)
tone_states = get_states(
[p[2] for p in pinyin], [p[2] for p in self.pinyin]
)
underline_states = get_states(
[p[0] + p[1] for p in pinyin], [p[0] + p[1] for p in self.pinyin]
)
for j in range(self.length):
char = idiom[j]
i2, f2, t2 = pinyin[j]
if char == self.idiom[j]:
block_color = self.colors.correct_color
char_color = initial_color = final_color = tone_color = (
self.colors.bg_color
)
underline = False
underline_color = ""
else:
block_color = self.colors.block_color
char_color = get_char_color(char_states[j])
initial_color = get_pinyin_color(initial_states[j])
final_color = get_pinyin_color(final_states[j])
tone_color = get_pinyin_color(tone_states[j])
underline_color = get_pinyin_color(underline_states[j])
underline = underline_color in (
self.colors.correct_color,
self.colors.exist_color,
)
block = self.draw_block(
block_color,
char,
char_color,
i2,
initial_color,
f2,
final_color,
t2,
tone_color,
underline,
underline_color,
)
board.paste(block, block_pos(i, j))
for i in range(len(self.guessed_idiom), rows):
for j in range(self.length):
block = self.draw_block(self.colors.block_color)
board.paste(block, block_pos(i, j))
return save_jpg(board)
def draw_hint(self) -> BytesIO:
guessed_char = set("".join(self.guessed_idiom))
guessed_initial = set()
guessed_final = set()
guessed_tone = set()
for pinyin in self.guessed_pinyin:
for p in pinyin:
guessed_initial.add(p[0])
guessed_final.add(p[1])
guessed_tone.add(p[2])
board_w = self.length * self.block_size[0]
board_w += (self.length - 1) * self.block_padding[0] + 2 * self.padding[0]
board_h = self.block_size[1] + 2 * self.padding[1]
board = Image.new("RGB", (board_w, board_h), self.colors.bg_color)
for i in range(self.length):
char = self.idiom[i]
hi, hf, ht = self.pinyin[i]
color = char_c = initial_c = final_c = tone_c = self.colors.correct_color
if char not in guessed_char:
char = "?"
color = self.colors.block_color
char_c = self.colors.wrong_color_char
else:
char_c = initial_c = final_c = tone_c = self.colors.bg_color
if hi not in guessed_initial:
hi = "?"
initial_c = self.colors.wrong_color_pinyin
if hf not in guessed_final:
hf = "?"
final_c = self.colors.wrong_color_pinyin
if ht not in guessed_tone:
ht = "?"
tone_c = self.colors.wrong_color_pinyin
block = self.draw_block(
color, char, char_c, hi, initial_c, hf, final_c, ht, tone_c
)
x = self.padding[0] + (self.block_size[0] + self.block_padding[0]) * i
y = self.padding[1]
board.paste(block, (x, y))
return save_jpg(board)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 MeetWq
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.

View File

@ -0,0 +1,113 @@
import json
import random
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Tuple
# from watchdog.observers import Observer
# from watchdog.events import FileSystemEventHandler, FileModifiedEvent
from PIL import ImageFont
from PIL.Image import Image as IMG
from PIL.ImageFont import FreeTypeFont
from pypinyin import Style, pinyin
resource_dir = Path(__file__).parent / "resources"
fonts_dir = resource_dir / "fonts"
data_dir = resource_dir / "data"
idiom_path = data_dir / "idioms.txt"
answer_path = data_dir / "answers.json"
LEGAL_PHRASES = [
idiom.strip() for idiom in idiom_path.open("r", encoding="utf-8").readlines()
]
ANSWER_PHRASES: List[Dict[str, str]] = json.load(answer_path.open("r", encoding="utf-8"))
# class LegalPhrasesModifiedHandler(FileSystemEventHandler):
# """
# Handler for resource file changes
# """
# def on_modified(self, event):
# print(f"{event.src_path} modified, reloading resource...")
# if "idioms.txt" in event.src_path:
# global LEGAL_PHRASES
# LEGAL_PHRASES = [
# idiom.strip()
# for idiom in idiom_path.open("r", encoding="utf-8").readlines()
# ]
# elif "answers.json" in event.src_path:
# global ANSWER_PHRASES
# ANSWER_PHRASES = json.load(
# answer_path.open("r", encoding="utf-8")
# )
# Observer().schedule(
# LegalPhrasesModifiedHandler(),
# data_dir,
# recursive=False,
# event_filter=FileModifiedEvent,
# )
def legal_idiom(word: str) -> bool:
return word in LEGAL_PHRASES
def random_idiom() -> Tuple[str, str]:
answer = random.choice(ANSWER_PHRASES)
return answer["word"], answer["explanation"]
# fmt: off
# 声母
INITIALS = [
"zh", "z", "y", "x", "w", "t", "sh", "s", "r", "q", "p",
"n", "m", "l", "k", "j", "h", "g", "f", "d", "ch", "c", "b"
]
# 韵母
FINALS = [
"ün", "üe", "üan", "ü", "uo", "un", "ui", "ue", "uang",
"uan", "uai","ua", "ou", "iu", "iong", "ong", "io", "ing",
"in", "ie", "iao", "iang", "ian", "ia", "er", "eng", "en",
"ei", "ao", "ang", "an", "ai", "u", "o", "i", "e", "a"
]
# fmt: on
def get_pinyin(idiom: str) -> List[Tuple[str, str, str]]:
pys = pinyin(idiom, style=Style.TONE3, v_to_u=True)
results = []
for p in pys:
py = p[0]
if py[-1].isdigit():
tone = py[-1]
py = py[:-1]
else:
tone = ""
initial = ""
for i in INITIALS:
if py.startswith(i):
initial = i
break
final = ""
for f in FINALS:
if py.endswith(f):
final = f
break
results.append((initial, final, tone)) # 声母,韵母,声调
return results
def save_jpg(frame: IMG) -> BytesIO:
output = BytesIO()
frame = frame.convert("RGB")
frame.save(output, format="jpeg")
return output
def load_font(name: str, fontsize: int) -> FreeTypeFont:
return ImageFont.truetype(str(fonts_dir / name), fontsize, encoding="utf-8")

View File

@ -0,0 +1,13 @@
## 版权声明
本插件由 汉钰律许可协议 授权开源,兼容并继承自 Apache 2.0 许可协议。
Copyright © 2024 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") with TriM Org.
版权所有 © 2024 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") with TriM Org.
伶伦转换器(trimo_plugin_msctconverter)根据 第一版 汉钰律许可协议(“本协议”)授权。\
任何人皆可从以下地址获得本协议副本:[汉钰律许可协议 第一版](https://gitee.com/EillesWan/YulvLicenses/raw/master/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE/%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.MD)。\
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。\
详细的准许和限制条款请见原协议文本。

View File

@ -0,0 +1,218 @@
**注意,以下条款或版权声明应当且必须是高于此仓库中任何其他声明的**
1. 伶伦的全部开发者享有其完整版权,其开发者可以在任一时刻终止以后伶伦源代码开放,若经由其开发者授予特殊权利,则授权对象可以将源代码进行特定的被特殊授权的操作
2. 伶伦或(及)其代码允许在 Apache2.0 协议的条款与说明下进行非商业使用
3. 除部分代码特殊声明外,伶伦允许对其或(及)其代码进行商业化使用,但是需要经过伶伦主要开发者(诸葛亮与八卦阵、金羿)的一致授权,同时,授权对象在商业化授权的使用过程中必须依照 Apache2.0 协议的条款与说明
4. 若存在对于伶伦包含的部分代码的特殊开源声明,则此部分代码依照其特定的开源方式授权,但若此部分代码经由此部分代码的主要开发者一致特殊授权后商用,则授权对象在商用时依照此部分的开发者所准许的方式(或条款)进行商用,在经此部分的开发者准许后无其他特殊授权时,默认依照 Apache2.0 协议进行商业化使用
5. Apache2.0 协议的英文原文副本可见下文
> The English Translation of the TERMS AND CONDITIONS above is listed below
>
> This translated version is for reference only and has no legal effect.
>
> The version with legal effect is the Chinese version above.
**Note, The TERMS AND CONDITIONS below should and must be above all others in this repository**
1. _Linglun Studio_ is fully copyrighted by all its developers, the developers have the right to make _Linglun Studio_ close sourced at any time. Operations are permitted under specific terms instructed by its developer(s).
2. Non-commercial use of _Linglun Studio_ and(or) its source code is permitted under Apache License 2.0.
3. Commercial use of _Linglun Studio_ is permitted under Apache License 2.0 with the unanimous permission of the steering developers of _Linglun Studio_ (*bgArray*诸葛亮与八卦阵 and *Eilles*金羿).
4. _Linglun Studio_ is open sourced under priority given:
1. License granted by the core developer(s) of a section after negotiation.
2. Explicitly stated license.
3. Apache 2.0 License.
5. A copy of the original Apache Lisence 2.0 can be found below.
```text
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright © 2024 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") with TriM Org.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```

View File

@ -0,0 +1,62 @@
def isin(sth: str, range_list: dict):
sth = sth.lower()
for bool_value, res_list in range_list.items():
if sth in res_list:
return bool_value
raise ValueError(
"不在可选范围内:{}".format([j for i in range_list.values() for j in i])
)
# 真假字符串判断
def bool_str(sth: str):
try:
return bool(float(sth))
except ValueError:
if str(sth).lower() in ("true", "", "", "y", "t"):
return True
elif str(sth).lower() in ("false", "", "", "f", "n"):
return False
else:
raise ValueError("非法逻辑字串")
def float_str(sth: str):
try:
return float(sth)
except ValueError:
return float(
sth.replace("", "1")
.replace("", "2")
.replace("", "3")
.replace("", "4")
.replace("", "5")
.replace("", "6")
.replace("", "7")
.replace("", "8")
.replace("", "9")
.replace("", "0")
.replace("", "1")
.replace("", "2")
.replace("", "3")
.replace("", "4")
.replace("", "5")
.replace("", "6")
.replace("", "7")
.replace("", "8")
.replace("", "9")
.replace("", "0")
.replace("", "0")
.replace("", "1")
.replace("", "2")
.replace("", "2")
.replace("", "7")
.replace("", ".")
)
def int_str(sth: str):
return int(float_str(sth))

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
'''
Copyright © 2022 Team-Ryoun
Licensed under the Apache License, Version 2.0 (the "License");
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
'''
# 自动转换exe指令格式
import uuid
def isfloatable(sth: str) -> bool:
try:float(sth);return True
except:return False
# 极限挑战
# execute @a[name="abc 123"] ~~ ~ execute @s ~9 346 ~-8 detect ^6 ^7 ^2 concrete 18 execute @p[r=3,scores={a=3}] 324 ~324 5 scoreboard players add @s[tag="999 888aasd asd "] QWE_AS 2
# /execute @a~~~/w @s aaa
# execute@s[tag="[]] 666"]~ 1~576detect^6^^66concrete 1 execute @s [scores={n=0}] ~ ~ ~0.09 execute@s~~~detect 0 0 0 bedrock -1 execute@a [name="999\"]]] jjj\""]~~ ~/execute@s[tag="℃♞\""]~ 32 ~5423 execute@s~~~detect ~~-1~ redstone_block 0 give @s [scores={name=12..}] command_block 1 1 {"name_tag":["\"a ":"b你 KKK\""]}
# 感谢 尘风、籽怼、邯潘阳(Happy2018New) 为本程序的试错提供了非常有效的支持
# 也感谢 尘风、Happy2018New、Dislink Sforza 为作者提供相关参考意见
def auto_translate(sentence:str):
'''传入一行旧的execute指令则将其转换为新格式
:param sentence: 旧的execute指令
:return: 新的execute指令
'''
if not 'execute' in sentence:
return sentence
elif 'run' in sentence:
return sentence[:sentence.find('run')+4]+auto_translate(sentence[sentence.find('run')+4:])
# 避免不规范的语法
sentence = ((__ := str(uuid.uuid4()),strings:=[(r"\"",__),], sentence.replace(r"\"",__)) if r"\"" in sentence else (None,strings:=[],sentence))[2]
# 如果有字符串包含其中
# 我们可以看作一个神奇的pattern
def foreSentence(sent,ptnA,ptnB):
startcatch = False
tempstring = ""
for i in sent:
if startcatch:
if i == ptnB:
startcatch = False
tpp = '{}'.format(tempstring)
tag = str(uuid.uuid4())
sent = sent.replace(tpp,tag)
strings.append((tpp,tag))
tempstring = ""
else:
tempstring += i
else:
if i == ptnA:
startcatch = True
# tempstring += i
# print(ptnA,ptnB,sent)
return sent
# print(sentence,"\n")
# 如果选择器的中括号包括空格
sentence = ((sentence[:sentence.find("@")+2]+sentence[sentence.find('['):]) if (sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence).replace("/"," ").lower()
sentence = foreSentence(foreSentence(foreSentence(sentence,'"','"'),"[","]"),"{","}")
list_sentence = list(sentence)
# 如果有神奇的东西在坐标后面,那就神奇了
for i in range(len(list_sentence)):
if list_sentence[i] in ('^','~'):
j = i + 1
while ((isfloatable("".join(list_sentence[i+1:j+1])))or(list_sentence[i+1] == "-" and isfloatable("".join(list_sentence[i+1:j+2]))))and j <= len(list_sentence):
j += 1
# print(j,"\t","".join(sentence[j:]))
if list_sentence[j] == " " or isfloatable(list_sentence[j]):
continue
else:
list_sentence.insert(j,' ')
sentence = "".join(list_sentence)
def backfor_sentence(a):
return [(a := a.replace(tag,tpp)) for tpp,tag in strings[::-1]][-1] if strings else a
# 下面是重点,只有我和老天爷看得懂
if 'detect' in sentence[:sentence.find("execute",8) if "execute" in sentence[8:] else -1]:
___ = [ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ]
____ = " ".join(___[3:]).split(" ")
return backfor_sentence('execute as {0} positioned as @s positioned {1} if block {2} {3} {4} at @s positioned {1} run {5}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(),sentence[(sentence.find("]") if "[" in sentence[:sentence.find("@")+3] else sentence.find("@")+1)+1:sentence.find("detect")-1].strip()," ".join(___[0:3]),____[0],____[1],auto_translate(" ".join(____[2:]))))
else:
___ = [ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[(sentence.find("]") if ("]" in sentence)and(sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence.find("@")+1)+1:].strip().split(" ",4)] for j in i ]
return backfor_sentence('execute as {0} positioned as @s positioned {1} at @s positioned {1} run {2}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(), " ".join(___[0:3]),auto_translate(" ".join(___[3:]))))
# 我是一个善良的人,没有用下面这个恶心你们
# backSentence('execute as {0} positioned as @s positioned {1} if block {2} {3} {4} at @s positioned {1} run {5}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(),sentence[(sentence.find("]") if "[" in sentence[:sentence.find("@")+3] else sentence.find("@")+1)+1:sentence.find("detect")-1].strip()," ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][0:3])," ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][3:]).split(" ")[0]," ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][3:]).split(" ")[1],autoTranslate(" ".join(" ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[sentence.find("detect")+6:].strip().split(" ",4)] for j in i ][3:]).split(" ")[2:])))) if 'detect' in sentence[:sentence.find("execute",8) if "execute" in sentence[8:] else -1] else backSentence('execute as {0} positioned as @s positioned {1} at @s positioned {1} run {2}'.format(sentence[sentence.find("execute")+7:(sentence.find("]") if "[" in sentence[:sentence.find("@")+5] else sentence.find("@")+1)+1].strip(), " ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[(sentence.find("]") if ("]" in sentence)and(sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence.find("@")+1)+1:].strip().split(" ",4)] for j in i ][0:3]),autoTranslate(" ".join([ j for i in [[i,] if sum([isfloatable(_) for _ in i]) else ((["~"+j for j in i[1:].split("~")] if i.startswith("~") else ["~"+j for j in i.split("~")]) if "~" in i else ([i,] if not "^" in i else (["^"+j for j in i[1:].split("^")] if i.startswith("^") else ["^"+j for j in i.split("^")]))) for i in sentence[(sentence.find("]") if ("]" in sentence)and(sum(0 if i == ' ' else 1 for i in sentence[sentence.find('@')+2:sentence.find('[')])==0) else sentence.find("@")+1)+1:].strip().split(" ",4)] for j in i ][3:]))))
def __main__():
'''主函数
'''
while True:
try:
sentence = input()
print()
print(auto_translate(sentence))
print("="*10)
except EOFError:
break
# except Exception as e:
# print(e)
# continue
if __name__ == "__main__":
__main__()
# 没写完,我也不知道咋写,但是总得写不是吗

View File

@ -0,0 +1,685 @@
import os
import sys
import time
import json
import shutil
import requests
from io import StringIO
from pathlib import Path
from typing import Annotated, Any
# from nonebot import require
import zhDateTime
import Musicreater
from Musicreater.plugin.bdxfile import to_BDX_file_in_delay, to_BDX_file_in_score
from Musicreater.plugin.addonpack import (
to_addon_pack_in_delay,
# to_addon_pack_in_repeater,
to_addon_pack_in_score,
)
from Musicreater.plugin.mcstructfile import (
to_mcstructure_file_in_delay,
# to_mcstructure_file_in_repeater,
to_mcstructure_file_in_score,
)
# import Musicreater.types
# import Musicreater.subclass
# import Musicreater.constants
import nonebot
import nonebot.adapters
import nonebot.drivers
import nonebot.rule
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.adapters.onebot.v11.event import GroupUploadNoticeEvent, GroupMessageEvent
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils import event as event_utils
from src.utils.base.language import get_user_lang
from src.utils.base.config import get_config
from .execute_auto_translator import auto_translate # type: ignore
nonebot.require("nonebot_plugin_alconna")
nonebot.require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import (
on_alconna,
Alconna,
Subcommand,
UniMessage,
Option,
store_true,
store_false,
store_value,
Arparma,
Args,
)
from nonebot_plugin_apscheduler import scheduler
(config_dir := Path(__file__).parent / "config").mkdir(exist_ok=True)
(database_dir := Path(__file__).parent / "db").mkdir(exist_ok=True)
temporary_dir = Path(__file__).parent / "temp"
if (config_path := config_dir / "config.json").exists():
configdict: dict = json.load(config_path.open("r", encoding="utf-8"))
else:
configdict = {
"donateCodePath": "https://foruda.gitee.com/images/1690165328188128032/d7f24fb5_9911226.jpeg",
"donateSite": "",
"helpPicPath": "https://foruda.gitee.com/images/1685873169584963569/95fe9b0b_9911226.png",
"maxCacheSize": 1048576,
"maxCacheTime": {".mcfunction": 900, ".mid .midi": 1800, ".json": 1800},
"maxSingleFileSize": {
".mcfunction": 524288,
".mid .midi": 131072,
".json": 8192,
},
"maxPersonConvert": {
"music": 20,
"structure": 20,
},
}
if (saves_path := database_dir / "file_saver.json").exists():
filesaves: dict = json.load(saves_path.open("r", encoding="utf-8"))
else:
filesaves = {}
max_cache_size = configdict["maxCacheSize"]
cache_limit_data: dict[str, tuple[int, int]] = {}
for cache_subtype in configdict["maxCacheTime"].keys():
for i in cache_subtype.split(" "):
cache_limit_data[i] = (
configdict["maxSingleFileSize"][cache_subtype],
configdict["maxCacheTime"][cache_subtype],
)
# if not os.path.exists("./temp/"):
# os.makedirs("./temp/")
def save_configdict():
json.dump(
configdict,
config_path.open("w", encoding="utf-8"),
indent=4,
ensure_ascii=False,
sort_keys=True,
)
def save_filesaves():
json.dump(
filesaves,
saves_path.open("w", encoding="utf-8"),
indent=4,
ensure_ascii=False,
sort_keys=True,
)
save_configdict()
save_filesaves()
enable_auto_exe_translate = {}
people_convert_times = {}
# 每天1点更新
@scheduler.scheduled_job("cron", hour=0)
async def every_day_update():
# ulang = Language(get_default_lang_code(), "zh-WY")
global people_convert_times
people_convert_times = {}
nonebot.logger.success("已重置每日转换次数")
@nonebot.get_driver().on_startup
async def _():
nonebot.logger.info("正在删除临时文件目录")
while temporary_dir.exists():
time.sleep(1)
try:
shutil.rmtree(temporary_dir)
except Exception as E:
nonebot.logger.warning(f"清空临时目录错误我真是栓Q{E}")
continue
os.makedirs(temporary_dir)
nonebot.logger.success("删除临时文件目录完成")
@scheduler.scheduled_job("interval", seconds=30)
async def _():
nonebot.logger.info(
"-删除文件检测-",
)
qqidlist = list(filesaves.keys()).copy()
save_file = False
for qqid in qqidlist:
namelist = list(filesaves[qqid].keys()).copy()
for name in namelist:
if name == "totalSize":
continue
elif (
zhDateTime.DateTime.now()
- zhDateTime.DateTime(*filesaves[qqid][name]["date"])
).seconds > cache_limit_data[os.path.splitext(name)[-1]][1]:
try:
os.remove(database_dir / qqid / name)
except:
pass
filesaves[qqid]["totalSize"] -= filesaves[qqid][name]["size"]
nonebot.logger.info(
"\t删除{}".format(name),
)
filesaves[qqid].pop(name)
save_file = True
try:
is_dir_empty = not os.listdir(
database_dir / qqid,
)
except:
is_dir_empty = True
if (
(filesaves[qqid]["totalSize"] <= 0)
or len(filesaves[qqid].keys()) == 1
or is_dir_empty
):
try:
shutil.rmtree(
database_dir / qqid,
)
except:
pass
filesaves.pop(qqid)
save_file = True
if save_file:
nonebot.logger.success("-已删除过期文件-")
save_filesaves()
else:
nonebot.logger.success("-无过期文件需要删除-")
# @nonebot.rule.Rule
# def file_receive_rule(event: GroupMessageEvent):
# return event.get_type() == "group_upload"
notece_ = nonebot.on_notice()
@notece_.handle()
async def _(
event: GroupUploadNoticeEvent,
bot: T_Bot,
):
# global cache_limit_data
file_infomation = event.model_dump()["file"]
file_subtype: str = os.path.splitext(file_infomation["name"])[-1].lower()
if file_subtype in cache_limit_data.keys():
if file_infomation["size"] > cache_limit_data[file_subtype][0]:
await notece_.finish(
"文件 {} 大小过大,这不是网盘\n单个{}文件不应大于 {} 千字节".format(
file_infomation["name"],
file_subtype.upper(),
cache_limit_data[file_subtype][0] / 1024,
),
at_sender=True,
)
return
elif (usr_id := str(event.user_id)) in filesaves.keys():
if (
filesaves[usr_id]["totalSize"] + file_infomation["size"]
> max_cache_size
):
await notece_.send(
"缓存容量已经耗尽,当前你在服务器内的占有为 {} 字节,合 {}/{} 千字节\n而服务器最多支持每个人占有 {} 兆字节(即 {} 字节)".format(
filesaves[usr_id]["totalSize"],
int(filesaves[usr_id]["totalSize"] / 10.24 + 0.5) / 100,
max_cache_size / 1024,
max_cache_size / 1048576,
max_cache_size,
),
at_sender=True,
)
await notece_.finish(
f"执行指令 清除缓存clearCache 以清除在服务器内存储的缓存文件。",
)
return
if file_infomation["name"] in filesaves[usr_id]:
await notece_.finish(
"你的缓存中已经包含了名称为 {} 的文件,不可重复上传。".format(
file_infomation["name"]
)
)
return
savepath = database_dir / usr_id
os.makedirs(savepath, exist_ok=True)
(savepath / file_infomation["name"]).open("wb").write(
requests.get(
file_infomation["url"],
verify=False,
).content
)
now = zhDateTime.DateTime.now()
try:
filesaves[usr_id][file_infomation["name"]] = {
"date": [
now.year,
now.month,
now.day,
now.hour,
now.minute,
],
"size": file_infomation["size"],
}
filesaves[usr_id]["totalSize"] += file_infomation["size"]
except:
filesaves[usr_id] = {
file_infomation["name"]: {
"date": [
now.year,
now.month,
now.day,
now.hour,
now.minute,
],
"size": file_infomation["size"],
}
}
filesaves[usr_id]["totalSize"] = file_infomation["size"]
save_filesaves()
await notece_.finish(
"文件 {} 已经保存,此文件在{:.1f}分内有效。".format(
file_infomation["name"], cache_limit_data[file_subtype][1] / 60
),
at_sender=True,
)
on_clear_cache = on_alconna(
command=Alconna("清除缓存"),
aliases={
"clearCache",
"clearcache",
"ClearCache",
"清除文件缓存",
"清除缓存文件",
"清空缓存",
},
)
@on_clear_cache.handle()
async def _(
event: GroupMessageEvent,
bot: T_Bot,
):
if (usr_id := str(event.user_id)) in filesaves.keys():
shutil.rmtree(database_dir / usr_id)
genText = (
"".join([i if i != "totalSize" else "" for i in filesaves[usr_id].keys()])
.replace("、、", "")
.strip("")
)
del filesaves[usr_id]
save_filesaves()
await on_clear_cache.finish(
UniMessage.text("文件 {} 已经清除。".format(genText)),
at_sender=True,
)
else:
await on_clear_cache.finish(
UniMessage.text("服务器内未存有阁下的缓存文件。"),
at_sender=True,
)
on_list_cache = on_alconna(
command=Alconna("查看缓存"),
aliases={"listCache", "listcache", "ListCache", "查看文件缓存", "查看缓存文件"},
)
@on_list_cache.handle()
async def _(
event: GroupMessageEvent,
bot: T_Bot,
):
if (usr_id := str(event.user_id)) in filesaves.keys():
genText = (
"\n".join(
[
(
"{}({}千字节): 剩余{}".format(
i,
int(j["size"] / 10.24 + 0.5) / 100,
cache_limit_data[os.path.splitext(i)[-1].lower()][1]
- (
zhDateTime.DateTime.now()
- zhDateTime.DateTime(*j["date"])
).seconds,
)
if i != "totalSize"
else ""
)
for i, j in filesaves[usr_id].items()
]
)
.replace("\n\n", "\n")
.strip("\n")
)
await on_list_cache.finish(
UniMessage.text(
"服务器中保有你的如下文件:\n{}\n共计 {}/{} 字节,合 {}/{} 千字节".format(
genText,
filesaves[usr_id]["totalSize"],
max_cache_size,
int(filesaves[usr_id]["totalSize"] / 10.24 + 0.5) / 100,
max_cache_size / 1024,
)
),
at_sender=True,
)
else:
await on_clear_cache.finish(
UniMessage.text("服务器内未存有阁下的缓存文件。"),
at_sender=True,
)
# def convert_midi(
# midi_file_path: str,
# play_speed: float = 1,
# default_tempo: int = Musicreater.mido.midifiles.midifiles.DEFAULT_TEMPO,
# pitched_note_table: Musicreater.MidiInstrumentTableType = Musicreater.MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
# percussion_note_table: Musicreater.MidiInstrumentTableType = Musicreater.MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
# old_exe_format: bool = False,
# min_volume: float = 0.1,
# vol_processing_func: Musicreater.FittingFunctionType = Musicreater.natural_curve,
# )
musicreater_convert = on_alconna(
aliases={"musicreater_convert", "音乐转换", "midi转换"},
command=Alconna(
"msctcvt",
Option("-f|--file", default="all", args=Args["file", str, "all"]), # ALL
Option("-emr|--enable-mismatch-error", default=False, action=store_true),
Option("-ps|--play-speed", default=1.0, args=Args["play-speed", float, 1.0]),
Option(
"-dftp|--default-tempo",
default=Musicreater.mido.midifiles.midifiles.DEFAULT_TEMPO,
args=Args[
"default-tempo", int, Musicreater.mido.midifiles.midifiles.DEFAULT_TEMPO
],
),
Option(
"-ptc|--pitched-note-table",
default="touch",
args=Args["pitched-note-table", str, "touch"],
),
Option(
"-pcs|--percussion-note-table",
default="touch",
args=Args["percussion-note-table", str, "touch"],
),
Option("-e|--old-execute-format", default=False, action=store_true),
Option(
"-mv|--minimal-volume", default=0.1, args=Args["minimal-volume", float, 0.1]
),
Option(
"-vpf|--volume-processing-function",
default="natural",
args=Args["volume-processing-function", str, "natural"],
),
Option("-t|-type", default="all", args=Args["type", str, "all"]),
Option(
"-pgb|--progress-bar",
default={
"base_s": r"%%N [ %%s/%^s %%% §e__________§r %%t|%^t ]",
"to_play_s": r"§7=",
"played_s": r"=",
},
args=Args["base_s", str, r"%%N [ %%s/%^s %%% §e__________§r %%t|%^t ]"][
"to_play_s", str, r"§7="
]["played_s", str, r"="],
),
Option(
"-s|--scoreboard-name",
default="mscplay",
args=Args["scoreboard-name", str, "mscplay"],
),
Option("-dsa|--disable-scoreboard-autoreset", default=False, action=store_true),
Option(
"-p|--player-selector",
default="@a",
args=Args["player-selector", str, "@a"],
),
Option("-h|--height-limit", default=32, args=Args["height-limit", int, 32]),
Option("-a|--author", default="Eilles", args=Args["author", str, "Eilles"]),
Option("-fa|--forward-axis", default="x+", args=Args["forward-axis", str, "x+"]),
),
permission=SUPERUSER,
)
@musicreater_convert.handle()
async def _(
result: Arparma,
event: GroupMessageEvent,
bot: T_Bot,
):
_args: dict = {
"file": "all",
"enable-mismatch-error": False,
"play-speed": 1.0,
"default-tempo": 500000,
"pitched-note-table": "touch",
"percussion-note-table": "touch",
"old-execute-format": False,
"minimal-volume": 0.1,
"volume-processing-function": "natural",
"type": "all",
"progress-bar": {
"base_s": r"%%N [ %%s/%^s %%% §e__________§r %%t|%^t ]",
"to_play_s": r"§7=",
"played_s": r"=",
},
"scoreboard-name": "mscplay",
"disable-scoreboard-autoreset": False,
"player-selector": "@a",
"height-limit": 32,
"author": "Eilles",
}
for arg in _args.keys():
_args[arg] = (
(
result.options[arg].args[arg]
if arg in result.options[arg].args.keys()
else result.options[arg].args
)
if (_vlu := result.options[arg].value) is None
else _vlu
)
# await musicreater_convert.finish(
# UniMessage.text(json.dumps(_args, indent=4, sort_keys=True, ensure_ascii=False))
# )
usr_id = str(event.user_id)
usr_data_path = database_dir / usr_id
(usr_temp_path := temporary_dir / usr_id).mkdir(exist_ok=True)
if (_ppnt:=_args["pitched-note-table"].lower() )in ["touch","classic","dislink"]:
pitched_notechart = Musicreater.MM_DISLINK_PITCHED_INSTRUMENT_TABLE if _ppnt == "dislink" else (Musicreater.MM_CLASSIC_PITCHED_INSTRUMENT_TABLE if _ppnt == "classic" else Musicreater.MM_TOUCH_PITCHED_INSTRUMENT_TABLE)
elif (_ppnt:=(usr_data_path / _args["pitched-note-table"])).exists():
pitched_notechart = Musicreater.MM_TOUCH_PITCHED_INSTRUMENT_TABLE.copy()
pitched_notechart.update(json.load(_ppnt.open("r")))
else:
await musicreater_convert.finish(UniMessage.text("乐器对照表 {} 不存在".format(_args["pitched-note-table"])))
return
if (_ppnt:=_args["percussion-note-table"].lower() )in ["touch","classic","dislink"]:
percussion_notechart = Musicreater.MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE if _ppnt == "dislink" else (Musicreater.MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE if _ppnt == "classic" else Musicreater.MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE)
elif (_ppnt:=(usr_data_path / _args["percussion-note-table"])).exists():
percussion_notechart = Musicreater.MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE.copy()
percussion_notechart.update(json.load(_ppnt.open("r")))
else:
await musicreater_convert.finish(UniMessage.text("乐器对照表 {} 不存在".format(_args["percussion-note-table"])))
return
if (_ppnt:=_args["volume-processing-function"].lower()) in ["natural","straight"]:
volume_curve = Musicreater.straight_line if _ppnt == "straight" else Musicreater.natural_curve
else:
await musicreater_convert.finish(UniMessage.text("音量处理曲线 {} 不存在".format(_args["volume-processing-function"])))
return
if (_ppnt:=_args["type"].lower()) == "all":
all_cvt_types = ["addon-delay","addon-score","mcstructure-dalay","mcstructure-score","bdx-delay","bdx-score",]
else:
all_cvt_types = _ppnt.split("&")
# 重定向标准输出
buffer = StringIO()
sys.stdout = buffer
sys.stderr = buffer
try:
progress_bar_style = Musicreater.ProgressBarStyle(**_args["progress-bar"])
all_files = []
for file_to_convert in (
filesaves[usr_id].keys()
if _args["file"].lower() == "all"
else _args["file"].split("&")
):
if file_to_convert.endswith(".mid") or file_to_convert.endswith(".midi"):
nonebot.logger.info("载入转换文件:", file_to_convert)
all_files.append(file_to_convert)
msct_obj = Musicreater.MidiConvert.from_midi_file(
midi_file_path=usr_data_path / file_to_convert,
mismatch_error_ignorance=not _args["enable-mismatch-error"],
play_speed=_args["play-speed"],
default_tempo=_args["default-tempo"],
pitched_note_table=pitched_notechart,
percussion_note_table=percussion_notechart,
old_exe_format=_args["old-execute-format"],
min_volume=_args["minimal-volume"],
vol_processing_func=volume_curve,
)
a = ["mcstructure-dalay","mcstructure-score","bdx-delay","bdx-score",]
if "addon-delay" in all_cvt_types:
to_addon_pack_in_delay(
midi_cvt=msct_obj,
dist_path=str(usr_temp_path),
progressbar_style=progress_bar_style,
player=_args["player-selector"],
max_height=_args["height-limit"],
)
all_cvt_types.remove("addon-delay")
if "addon-score" in all_cvt_types:
to_addon_pack_in_score()
if not all_files:
nonebot.logger.warning("无可供转换的文件",)
await musicreater_convert.send(UniMessage("不是哥们,空气咱这转不成面包,那是中科院的事。"))
except Exception as e:
nonebot.logger.error("转换存在错误:",e)
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
execute_cmd_convert_ablity = on_alconna(
command=Alconna("指令自动更新"),
aliases={
"指令更新自动",
"自动指令更新",
"指令更新",
},
)
@execute_cmd_convert_ablity.handle()
async def _(
event: T_MessageEvent,
bot: T_Bot,
):
ulang = get_user_lang(usrid := str(event_utils.get_user_id(event)))
global enable_auto_exe_translate
enable_auto_exe_translate[usrid] = not enable_auto_exe_translate.get(usrid, True)
await execute_cmd_convert_ablity.finish(
UniMessage.text(
ulang.get(
"upexecute.enable"
if enable_auto_exe_translate[usrid]
else "upexecute.disable"
)
)
)
execute_cmd_convert = nonebot.on_startswith(
"execute",
)
@execute_cmd_convert.handle()
async def _(
event: T_MessageEvent,
bot: T_Bot,
):
global enable_auto_exe_translate
if not enable_auto_exe_translate.get(
usrid := str(event_utils.get_user_id(event)), True
):
execute_cmd_convert.destroy()
return
ulang = get_user_lang(usrid)
if (
result_execmd := auto_translate(event.get_plaintext())
) == event.get_plaintext():
await execute_cmd_convert.finish(ulang.get("upexecute.same"))
else:
await execute_cmd_convert.finish(result_execmd)
# test_exec = nonebot.on_command(
# "test-exec",
# rule=to_me(),
# permission=SUPERUSER,
# )
# @test_exec.handle()
# async def _(args: Annotated[nonebot.adapters.Message, CommandArg()]):
# await test_exec.finish(exec(args.extract_plain_text()))

View File

@ -12,7 +12,7 @@
height: 100px;
display: flex;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 100px;
/* border-radius: 100px; */
margin-bottom: 10px;
padding-right: 10px;
}
@ -25,7 +25,7 @@
}
.row-icon {
border-radius: 50%;
/* border-radius: 50%; */
margin-right: auto;
}

View File

@ -0,0 +1,4 @@
upexecute.same=指令转换前后一致
upexecute.enable=execute指令自动更新已启用
upexecute.disable=已禁用execute指令自动更新

View File

@ -0,0 +1,4 @@
upexecute.same=令未变
upexecute.enable=更令之法方用
upexecute.disable=更令之法方去

View File

@ -0,0 +1,3 @@
name: 伶伦转换器附件
description: 伶伦转换器所用之资源包
ersion: 2024.7.19

View File

@ -163,4 +163,7 @@ yanlun.refresh.failed=更新 言·论 信息发生 {ERR} 错误:{ERRCODE}
yanlun.errtype.net=互联网连接
yanlun.errtype.unknown=未知
yanlun.count.head=出处ttt数量(占比)
yanlun.count.tail=...(共 {NUM} 条)
yanlun.count.tail=...(共 {NUM} 条)
yanlun.length.toolong=言·论过长
yanlun.length.tooshort=言·论过短
yanlun.length.float=言·论不可是非整数长度

View File

@ -163,4 +163,7 @@ yanlun.refresh.failed=言·论 因{ERR}而无以新:{ERRCODE}
yanlun.errtype.net=遥讯不得
yanlun.errtype.unknown=无名之亏
yanlun.count.head=所缘ttt几何(比率)
yanlun.count.tail=...(合{NUM}条)
yanlun.count.tail=...(合{NUM}条)
yanlun.length.toolong=繁言勿论
yanlun.length.tooshort=无言以论
yanlun.length.float=此言不获世之滋垢,皭然泥而不滓者也,故不论之

View File

@ -18,7 +18,7 @@
</div>
<div class="bot-detail">
<div class="bot-name" style="text-align:start">
TriM-Liteyuki
TriMO-Liteyuki
</div>
<hr>
<div class="bot-icon-tags">

View File

@ -6,7 +6,7 @@ import time
import nonebot
__NAME__ = "LiteyukiBot-Trim"
__NAME__ = "LiteyukiBot-TriMO"
__VERSION__ = "6.3.3" # 60201
# __VERSION_I__ = 99060303

View File

@ -43,9 +43,9 @@ def load_from_lang(file_path: str, lang_code: str = None):
if lang_code not in _language_data:
_language_data[lang_code] = {}
_language_data[lang_code].update(data)
nonebot.logger.debug(f"Loaded language data from {file_path}")
nonebot.logger.debug(f"已从 {file_path} 读取语言文件")
except Exception as e:
nonebot.logger.error(f"Failed to load language data from {file_path}: {e}")
nonebot.logger.error(f"无法读取以下目录中的语言文件 {file_path},详阅:{e}")
def load_from_json(file_path: str, lang_code: str = None):
@ -196,7 +196,7 @@ def get_user_lang(user_id: str) -> Language:
user_id = str(user_id)
if user_id not in _user_lang:
nonebot.logger.debug(f"Loading user language for {user_id}")
nonebot.logger.debug(f"正为 {user_id} 加载语言包…")
user = user_db.where_one(
User(),
"user_id = ?",

View File

@ -6,7 +6,7 @@ from os import getcwd
import aiofiles
import nonebot
from nonebot_plugin_htmlrender import *
from nonebot_plugin_htmlrender import * # type: ignore
from .tools import random_hex_string
# import imgkit

View File

@ -34,7 +34,9 @@ async def broadcast_to_superusers(message: str | T_Message, markdown: bool = Fal
for bot in nonebot.get_bots().values():
for user_id in config.get("superusers", []):
if markdown:
await MarkdownMessage.send_md(message, bot, message_type="private", session_id=user_id)
await MarkdownMessage.send_md(
message, bot, message_type="private", session_id=user_id
)
else:
await bot.send_private_msg(user_id=user_id, message=message)
@ -42,13 +44,14 @@ async def broadcast_to_superusers(message: str | T_Message, markdown: bool = Fal
class MarkdownMessage:
@staticmethod
async def send_md(
markdown: str,
bot: T_Bot, *,
message_type: str = None,
session_id: str | int = None,
event: T_MessageEvent = None,
retry_as_image: bool = True,
**kwargs
markdown: str,
bot: T_Bot,
*,
message_type: str = None,
session_id: str | int = None,
event: T_MessageEvent = None,
retry_as_image: bool = True,
**kwargs,
) -> dict[str, Any] | None:
"""
发送Markdown消息支持自动转为图片发送
@ -64,7 +67,7 @@ class MarkdownMessage:
Returns:
"""
formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r'\\\"')
formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r"\\\"")
if event is not None and message_type is None:
if isinstance(event, satori.event.Event):
message_type = "private" if event.guild is None else "group"
@ -73,83 +76,81 @@ class MarkdownMessage:
assert event is not None
message_type = event.message_type
group_id = event.group_id if message_type == "group" else None
user_id = event.user.id if isinstance(event, satori.event.Event) else event.user_id
user_id = (
event.user.id
if isinstance(event, satori.event.Event)
else event.user_id
)
session_id = user_id if message_type == "private" else group_id
try:
raise TencentBannedMarkdownError("Tencent banned markdown")
forward_id = await bot.call_api(
"send_private_forward_msg",
messages=[
{
"type": "node",
"data": {
"content": [
{
"data": {
"content": "{\"content\":\"%s\"}" % formatted_md,
},
"type": "markdown"
}
],
"name": "[]",
"uin": bot.self_id
}
}
],
user_id=bot.self_id
)
# try:
# raise TencentBannedMarkdownError("Tencent banned markdown")
# forward_id = await bot.call_api(
# "send_private_forward_msg",
# messages=[
# {
# "type": "node",
# "data": {
# "content": [
# {
# "data": {
# "content": "{\"content\":\"%s\"}" % formatted_md,
# },
# "type": "markdown"
# }
# ],
# "name": "[]",
# "uin": bot.self_id
# }
# }
# ],
# user_id=bot.self_id
# )
# data = await bot.send_msg(
# user_id=session_id,
# group_id=session_id,
# message_type=message_type,
# message=[
# {
# "type": "longmsg",
# "data": {
# "id": forward_id
# }
# },
# ],
# **kwargs
# )
# except BaseException as e:
nonebot.logger.error(f"因未能发送Markdown消息已转为图片发送。")
# 发送失败,渲染为图片发送
# if not retry_as_image:
# return None
# plain_markdown = markdown.replace("[🔗", "[")
md_image_bytes = await md_to_pic(md=markdown, width=540, device_scale_factor=4)
if isinstance(bot, satori.Bot):
msg_seg = satori.MessageSegment.image(raw=md_image_bytes, mime="image/png")
data = await bot.send(event=event, message=msg_seg)
else:
data = await bot.send_msg(
user_id=session_id,
group_id=session_id,
message_type=message_type,
message=[
{
"type": "longmsg",
"data": {
"id": forward_id
}
},
],
**kwargs
group_id=session_id,
user_id=session_id,
message=v11.MessageSegment.image(md_image_bytes),
)
except BaseException as e:
nonebot.logger.error(f"send markdown error, retry as image: {e}")
# 发送失败,渲染为图片发送
# if not retry_as_image:
# return None
plain_markdown = markdown.replace("[🔗", "[")
md_image_bytes = await md_to_pic(
md=plain_markdown,
width=540,
device_scale_factor=4
)
if isinstance(bot, satori.Bot):
msg_seg = satori.MessageSegment.image(raw=md_image_bytes,mime="image/png")
data = await bot.send(
event=event,
message=msg_seg
)
else:
data = await bot.send_msg(
message_type=message_type,
group_id=session_id,
user_id=session_id,
message=v11.MessageSegment.image(md_image_bytes),
)
return data
@staticmethod
async def send_image(
image: bytes | str,
bot: T_Bot, *,
message_type: str = None,
session_id: str | int = None,
event: T_MessageEvent = None,
**kwargs
image: bytes | str,
bot: T_Bot,
*,
message_type: str = None,
session_id: str | int = None,
event: T_MessageEvent = None,
**kwargs,
) -> dict:
"""
发送单张装逼大图
@ -181,23 +182,35 @@ class MarkdownMessage:
if method == 2:
base64_string = base64.b64encode(image).decode("utf-8")
data = await bot.call_api("upload_image", file=f"base64://{base64_string}")
await MarkdownMessage.send_md(MarkdownMessage.image(data, Image.open(io.BytesIO(image)).size), bot,
event=event, message_type=message_type,
session_id=session_id, **kwargs)
await MarkdownMessage.send_md(
MarkdownMessage.image(data, Image.open(io.BytesIO(image)).size),
bot,
event=event,
message_type=message_type,
session_id=session_id,
**kwargs,
)
# 其他实现端方案
else:
image_message_id = (await bot.send_private_msg(
user_id=bot.self_id,
message=[
v11.MessageSegment.image(file=image)
]
))["message_id"]
image_url = (await bot.get_msg(message_id=image_message_id))["message"][0]["data"]["url"]
image_message_id = (
await bot.send_private_msg(
user_id=bot.self_id, message=[v11.MessageSegment.image(file=image)]
)
)["message_id"]
image_url = (await bot.get_msg(message_id=image_message_id))["message"][0][
"data"
]["url"]
image_size = Image.open(io.BytesIO(image)).size
image_md = MarkdownMessage.image(image_url, image_size)
return await MarkdownMessage.send_md(image_md, bot, message_type=message_type, session_id=session_id,
event=event, **kwargs)
return await MarkdownMessage.send_md(
image_md,
bot,
message_type=message_type,
session_id=session_id,
event=event,
**kwargs,
)
if data is None:
data = await bot.send_msg(
@ -205,7 +218,7 @@ class MarkdownMessage:
group_id=session_id,
user_id=session_id,
message=v11.MessageSegment.image(image),
**kwargs
**kwargs,
)
return data
@ -232,7 +245,9 @@ class MarkdownMessage:
markdown格式的可点击回调按钮
"""
if "" not in config.get("command_start", ["/"]) and config.get("alconna_use_command_start", False):
if "" not in config.get("command_start", ["/"]) and config.get(
"alconna_use_command_start", False
):
cmd = f"{config['command_start'][0]}{cmd}"
return f"[{name}](mqqapi://aio/inlinecmd?command={quote(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})"

View File

@ -1,15 +1,15 @@
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
__plugin_meta__ = PluginMetadata(
name="Minecraft工具箱",
description="一些Minecraft相关工具箱",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
__plugin_meta__ = PluginMetadata(
name="Minecraft工具箱",
description="一些Minecraft相关工具箱",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)

View File

@ -1,20 +1,20 @@
from nonebot.plugin import PluginMetadata
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪包管理器v2",
description="详细看文档",
usage=(
"npm list\n"
"npm enable/disable <plugin_name>\n"
"npm search <keywords...>\n"
"npm install/uninstall <plugin_name>\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : False,
}
)
from nonebot.plugin import PluginMetadata
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪包管理器v2",
description="详细看文档",
usage=(
"npm list\n"
"npm enable/disable <plugin_name>\n"
"npm search <keywords...>\n"
"npm install/uninstall <plugin_name>\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : False,
}
)

View File

@ -1,27 +1,27 @@
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
from .qweather import *
__plugin_meta__ = PluginMetadata(
name="轻雪天气",
description="基于和风天气api的天气插件",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
from ...utils.base.data_manager import set_memory_data
driver = get_driver()
@driver.on_startup
async def _():
# 检查是否为开发者模式
is_dev = await check_key_dev(get_config("weather_key", ""))
set_memory_data("weather.is_dev", is_dev)
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
from .qweather import *
__plugin_meta__ = PluginMetadata(
name="轻雪天气",
description="基于和风天气api的天气插件",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
from ...utils.base.data_manager import set_memory_data
driver = get_driver()
@driver.on_startup
async def _():
# 检查是否为开发者模式
is_dev = await check_key_dev(get_config("weather_key", ""))
set_memory_data("weather.is_dev", is_dev)

View File

@ -1,171 +1,171 @@
import aiohttp
from .qw_models import *
import httpx
from ...utils.base.data_manager import get_memory_data
from ...utils.base.language import Language
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境
def get_qw_lang(lang: str) -> str:
if lang in ["zh-HK", "zh-TW"]:
return "zh-hant"
elif lang.startswith("zh"):
return "zh"
elif lang.startswith("en"):
return "en"
else:
return lang
async def check_key_dev(key: str) -> bool:
url = "https://api.qweather.com/v7/weather/now?"
params = {
"location": "101010100",
"key" : key,
}
async with aiohttp.ClientSession() as client:
resp = await client.get(url, params=params)
return (await resp.json()).get("code") != "200" # 查询不到付费数据为开发版
def get_local_data(ulang_code: str) -> dict:
"""
获取本地化数据
Args:
ulang_code:
Returns:
"""
ulang = Language(ulang_code)
return {
"monday" : ulang.get("weather.monday"),
"tuesday" : ulang.get("weather.tuesday"),
"wednesday": ulang.get("weather.wednesday"),
"thursday" : ulang.get("weather.thursday"),
"friday" : ulang.get("weather.friday"),
"saturday" : ulang.get("weather.saturday"),
"sunday" : ulang.get("weather.sunday"),
"today" : ulang.get("weather.today"),
"tomorrow" : ulang.get("weather.tomorrow"),
"day" : ulang.get("weather.day"),
"night" : ulang.get("weather.night"),
"no_aqi" : ulang.get("weather.no_aqi"),
}
async def city_lookup(
location: str,
key: str,
adm: str = "",
number: int = 20,
lang: str = "zh",
) -> CityLookup:
"""
通过关键字搜索城市信息
Args:
location:
key:
adm:
number:
lang: 可传入标准i18n语言代码如zh-CNen-US等
Returns:
"""
url = "https://geoapi.qweather.com/v2/city/lookup?"
params = {
"location": location,
"adm" : adm,
"number" : number,
"key" : key,
"lang" : lang,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return CityLookup.parse_obj(resp.json())
async def get_weather_now(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/now?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_daily(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dd?" % (7 if dev else 30)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_hourly(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dh?" % (24 if dev else 168)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_airquality(
key: str,
location: str,
lang: str,
pollutant: bool = False,
station: bool = False,
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = f"airquality/v1/now/{location}?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"key" : key,
"lang" : lang,
"pollutant": pollutant,
"station" : station,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
import aiohttp
from .qw_models import *
import httpx
from ...utils.base.data_manager import get_memory_data
from ...utils.base.language import Language
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境
def get_qw_lang(lang: str) -> str:
if lang in ["zh-HK", "zh-TW"]:
return "zh-hant"
elif lang.startswith("zh"):
return "zh"
elif lang.startswith("en"):
return "en"
else:
return lang
async def check_key_dev(key: str) -> bool:
url = "https://api.qweather.com/v7/weather/now?"
params = {
"location": "101010100",
"key" : key,
}
async with aiohttp.ClientSession() as client:
resp = await client.get(url, params=params)
return (await resp.json()).get("code") != "200" # 查询不到付费数据为开发版
def get_local_data(ulang_code: str) -> dict:
"""
获取本地化数据
Args:
ulang_code:
Returns:
"""
ulang = Language(ulang_code)
return {
"monday" : ulang.get("weather.monday"),
"tuesday" : ulang.get("weather.tuesday"),
"wednesday": ulang.get("weather.wednesday"),
"thursday" : ulang.get("weather.thursday"),
"friday" : ulang.get("weather.friday"),
"saturday" : ulang.get("weather.saturday"),
"sunday" : ulang.get("weather.sunday"),
"today" : ulang.get("weather.today"),
"tomorrow" : ulang.get("weather.tomorrow"),
"day" : ulang.get("weather.day"),
"night" : ulang.get("weather.night"),
"no_aqi" : ulang.get("weather.no_aqi"),
}
async def city_lookup(
location: str,
key: str,
adm: str = "",
number: int = 20,
lang: str = "zh",
) -> CityLookup:
"""
通过关键字搜索城市信息
Args:
location:
key:
adm:
number:
lang: 可传入标准i18n语言代码如zh-CNen-US等
Returns:
"""
url = "https://geoapi.qweather.com/v2/city/lookup?"
params = {
"location": location,
"adm" : adm,
"number" : number,
"key" : key,
"lang" : lang,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return CityLookup.parse_obj(resp.json())
async def get_weather_now(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/now?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_daily(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dd?" % (7 if dev else 30)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_hourly(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dh?" % (24 if dev else 168)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_airquality(
key: str,
location: str,
lang: str,
pollutant: bool = False,
station: bool = False,
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = f"airquality/v1/now/{location}?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"key" : key,
"lang" : lang,
"pollutant": pollutant,
"station" : station,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()

View File

@ -1,62 +1,62 @@
from src.utils.base.data import LiteModel
class Location(LiteModel):
name: str = ""
id: str = ""
lat: str = ""
lon: str = ""
adm2: str = ""
adm1: str = ""
country: str = ""
tz: str = ""
utcOffset: str = ""
isDst: str = ""
type: str = ""
rank: str = ""
fxLink: str = ""
sources: str = ""
license: str = ""
class CityLookup(LiteModel):
code: str = ""
location: list[Location] = [Location()]
class Now(LiteModel):
obsTime: str = ""
temp: str = ""
feelsLike: str = ""
icon: str = ""
text: str = ""
wind360: str = ""
windDir: str = ""
windScale: str = ""
windSpeed: str = ""
humidity: str = ""
precip: str = ""
pressure: str = ""
vis: str = ""
cloud: str = ""
dew: str = ""
sources: str = ""
license: str = ""
class WeatherNow(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
now: Now = Now()
class Daily(LiteModel):
pass
class WeatherDaily(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
daily: list[str] = []
from src.utils.base.data import LiteModel
class Location(LiteModel):
name: str = ""
id: str = ""
lat: str = ""
lon: str = ""
adm2: str = ""
adm1: str = ""
country: str = ""
tz: str = ""
utcOffset: str = ""
isDst: str = ""
type: str = ""
rank: str = ""
fxLink: str = ""
sources: str = ""
license: str = ""
class CityLookup(LiteModel):
code: str = ""
location: list[Location] = [Location()]
class Now(LiteModel):
obsTime: str = ""
temp: str = ""
feelsLike: str = ""
icon: str = ""
text: str = ""
wind360: str = ""
windDir: str = ""
windScale: str = ""
windSpeed: str = ""
humidity: str = ""
precip: str = ""
pressure: str = ""
vis: str = ""
cloud: str = ""
dew: str = ""
sources: str = ""
license: str = ""
class WeatherNow(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
now: Now = Now()
class Daily(LiteModel):
pass
class WeatherDaily(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
daily: list[str] = []

View File

@ -1,101 +1,101 @@
from nonebot import require, on_endswith
from nonebot.adapters import satori
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.internal.matcher import Matcher
from src.utils.base.config import get_config
from src.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from src.utils.base.data_manager import User, user_db
from src.utils.base.language import Language, get_user_lang
from src.utils.base.resource import get_path
from src.utils.message.html_tool import template2image
from src.utils import event as event_utils
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma, UniMessage
wx_alc = on_alconna(
aliases={"天气"},
command=Alconna(
"weather",
Args["keywords", MultiVar(str), []],
),
)
@wx_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
kws = result.main_args.get("keywords")
image = await get_weather_now_card(matcher, event, kws)
await wx_alc.finish(UniMessage.image(raw=image))
@on_endswith(("天气", "weather")).handle()
async def _(event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
# kws = event.message.extract_plain_text()
kws = event.get_plaintext()
image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False)
if isinstance(event, satori.event.Event):
await matcher.finish(satori.MessageSegment.image(raw=image, mime="image/png"))
else:
await matcher.finish(MessageSegment.image(image))
async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True):
ulang = get_user_lang(event_utils.get_user_id(event))
qw_lang = get_qw_lang(ulang.lang_code)
key = get_config("weather_key")
is_dev = get_memory_data("weather.is_dev", True)
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), default=User())
# params
unit = user.profile.get("unit", "m")
stored_location = user.profile.get("location", None)
if not key:
await matcher.finish(ulang.get("weather.no_key") if tip else None)
if keyword:
if len(keyword) >= 2:
adm = keyword[0]
city = keyword[-1]
else:
adm = ""
city = keyword[0]
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
city_name = " ".join(keyword)
else:
if not stored_location:
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None)
city_info = await city_lookup(stored_location, key, lang=qw_lang)
city_name = stored_location
if city_info.code == "200":
location_data = city_info.location[0]
else:
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None)
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
image = await template2image(
template=get_path("templates/weather_now.html", abs_path=True),
templates={
"data": {
"params" : {
"unit": unit,
"lang": ulang.lang_code,
},
"weatherNow" : weather_now,
"weatherDaily" : weather_daily,
"weatherHourly": weather_hourly,
"aqi" : aqi,
"location" : location_data.dump(),
"localization" : get_local_data(ulang.lang_code)
}
},
)
return image
from nonebot import require, on_endswith
from nonebot.adapters import satori
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.internal.matcher import Matcher
from src.utils.base.config import get_config
from src.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from src.utils.base.data_manager import User, user_db
from src.utils.base.language import Language, get_user_lang
from src.utils.base.resource import get_path
from src.utils.message.html_tool import template2image
from src.utils import event as event_utils
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma, UniMessage
wx_alc = on_alconna(
aliases={"天气"},
command=Alconna(
"weather",
Args["keywords", MultiVar(str), []],
),
)
@wx_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
kws = result.main_args.get("keywords")
image = await get_weather_now_card(matcher, event, kws)
await wx_alc.finish(UniMessage.image(raw=image))
@on_endswith(("天气", "weather")).handle()
async def _(event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
# kws = event.message.extract_plain_text()
kws = event.get_plaintext()
image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False)
if isinstance(event, satori.event.Event):
await matcher.finish(satori.MessageSegment.image(raw=image, mime="image/png"))
else:
await matcher.finish(MessageSegment.image(image))
async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True):
ulang = get_user_lang(event_utils.get_user_id(event))
qw_lang = get_qw_lang(ulang.lang_code)
key = get_config("weather_key")
is_dev = get_memory_data("weather.is_dev", True)
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event), default=User())
# params
unit = user.profile.get("unit", "m")
stored_location = user.profile.get("location", None)
if not key:
await matcher.finish(ulang.get("weather.no_key") if tip else None)
if keyword:
if len(keyword) >= 2:
adm = keyword[0]
city = keyword[-1]
else:
adm = ""
city = keyword[0]
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
city_name = " ".join(keyword)
else:
if not stored_location:
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None)
city_info = await city_lookup(stored_location, key, lang=qw_lang)
city_name = stored_location
if city_info.code == "200":
location_data = city_info.location[0]
else:
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None)
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
image = await template2image(
template=get_path("templates/weather_now.html", abs_path=True),
templates={
"data": {
"params" : {
"unit": unit,
"lang": ulang.lang_code,
},
"weatherNow" : weather_now,
"weatherDaily" : weather_daily,
"weatherHourly": weather_hourly,
"aqi" : aqi,
"location" : location_data.dump(),
"localization" : get_local_data(ulang.lang_code)
}
},
)
return image

View File

@ -1,12 +1,12 @@
weather.monday=Mon
weather.tuesday=Tue
weather.wednesday=Wed
weather.thursday=Thu
weather.friday=Fri
weather.saturday=Sat
weather.sunday=Sun
weather.day=Day
weather.night=Night
weather.today=Today
weather.tomorrow=Tomorrow
weather.monday=Mon
weather.tuesday=Tue
weather.wednesday=Wed
weather.thursday=Thu
weather.friday=Fri
weather.saturday=Sat
weather.sunday=Sun
weather.day=Day
weather.night=Night
weather.today=Today
weather.tomorrow=Tomorrow
weather.no_aqi=No AQI data

View File

@ -1,12 +1,12 @@
weather.monday=月
weather.tuesday=火
weather.wednesday=水
weather.thursday=木
weather.friday=金
weather.saturday=土
weather.sunday=日
weather.day=昼
weather.night=夜
weather.today=今日
weather.tomorrow=明日
weather.monday=月
weather.tuesday=火
weather.wednesday=水
weather.thursday=木
weather.friday=金
weather.saturday=土
weather.sunday=日
weather.day=昼
weather.night=夜
weather.today=今日
weather.tomorrow=明日
weather.no_aqi=空気質データなし

View File

@ -1,12 +1,12 @@
weather.monday=周一
weather.tuesday=周二
weather.wednesday=周三
weather.thursday=周四
weather.friday=周五
weather.saturday=周六
weather.sunday=周日
weather.day=白天
weather.night=夜晚
weather.today=今天
weather.tomorrow=明天
weather.monday=周一
weather.tuesday=周二
weather.wednesday=周三
weather.thursday=周四
weather.friday=周五
weather.saturday=周六
weather.sunday=周日
weather.day=白天
weather.night=夜晚
weather.today=今天
weather.tomorrow=明天
weather.no_aqi=暂无AQI数据

View File

@ -1,3 +1,3 @@
name: 轻雪天气资源包
description: For Liteyuki Weather
name: 轻雪天气资源包
description: For Liteyuki Weather
version: 2024.4.26

View File

@ -1,184 +1,184 @@
:root {
--main-text-color: #fff;
--sub-text-color: #ccc;
--tip-text-color: #999;
--device-info-width: 240px;
--sub-border-radius: 60px;
}
#weather-info {
color: white;
/*justify-content: center;*/
/*align-items: center;*/
/*align-content: center;*/
}
.icon {
/* icon 类img阴影*/
filter: drop-shadow(1px 1px 10px #00000044);
}
#main-info {
display: flex;
justify-content: center;
align-items: center;
}
#main-left {
display: flex;
justify-content: flex-end;
width: 50%;
}
#main-right {
width: 50%;
}
#time {
font-size: 25px;
font-weight: bold;
font-style: italic;
text-align: right;
color: var(--sub-text-color);
}
#adm {
font-size: 32px;
font-weight: bold;
text-align: center;
color: var(--sub-text-color);
}
#city {
margin-top: 20px;
font-size: 70px;
font-weight: bold;
text-align: center;
}
#temperature {
display: flex;
align-items: baseline;
}
#temperature-now {
font-size: 70px;
font-weight: bold;
}
#temperature-range {
font-size: 40px;
font-weight: bold;
color: var(--sub-text-color);
}
#description {
font-size: 50px;
font-weight: bold;
}
#aqi {
height: 50px;
display: flex;
border-radius: 60px;
padding: 5px;
font-size: 40px;
text-align: center;
align-content: center;
align-items: center;
justify-content: center;
}
#aqi-dot {
height: 80%;
aspect-ratio: 1 / 1;
border-radius: 50%;
background-color: var(--sub-text-color);
margin-right: 20px;
}
.main-icon {
width: 240px;
height: 240px;
}
#hours-info {
display: flex;
justify-content: space-between;
}
.hourly-item {
text-align: center;
background-color: #ffffff44;
border-radius: var(--sub-border-radius);
align-items: center;
padding: 20px 10px;
}
.hourly-icon{
width: 80%;
margin-bottom: 20px;
}
.hourly-temperature {
text-align: center;
color: var(--main-text-color);
font-size: 30px;
margin-bottom: 20px;
}
.hourly-time {
text-align: center;
color: var(--main-text-color);
font-size: 25px;
margin-bottom: 10px;
}
/**/
.daily-item {
display: flex;
position: relative;
justify-content: space-between;
align-items: center;
background-color: #ffffff44;
height: 90px;
border-radius: var(--sub-border-radius);
margin-bottom: 20px;
padding: 0 30px;
}
/*最后一个没有margin_button*/
.daily-item:last-child {
margin-bottom: 0;
}
.icon-day {
position: absolute;
left: 60%;
height: 80px;
}
.icon-night {
position: absolute;
left: 70%;
height: 80px;
}
.daily-weather{
position: absolute;
left: 30%;
}
.daily-temperature{
position: absolute;
left: 83%;
}
.daily-day, .daily-weather, .daily-temperature {
text-align: center;
color: var(--main-text-color);
font-size: 30px;
:root {
--main-text-color: #fff;
--sub-text-color: #ccc;
--tip-text-color: #999;
--device-info-width: 240px;
--sub-border-radius: 60px;
}
#weather-info {
color: white;
/*justify-content: center;*/
/*align-items: center;*/
/*align-content: center;*/
}
.icon {
/* icon 类img阴影*/
filter: drop-shadow(1px 1px 10px #00000044);
}
#main-info {
display: flex;
justify-content: center;
align-items: center;
}
#main-left {
display: flex;
justify-content: flex-end;
width: 50%;
}
#main-right {
width: 50%;
}
#time {
font-size: 25px;
font-weight: bold;
font-style: italic;
text-align: right;
color: var(--sub-text-color);
}
#adm {
font-size: 32px;
font-weight: bold;
text-align: center;
color: var(--sub-text-color);
}
#city {
margin-top: 20px;
font-size: 70px;
font-weight: bold;
text-align: center;
}
#temperature {
display: flex;
align-items: baseline;
}
#temperature-now {
font-size: 70px;
font-weight: bold;
}
#temperature-range {
font-size: 40px;
font-weight: bold;
color: var(--sub-text-color);
}
#description {
font-size: 50px;
font-weight: bold;
}
#aqi {
height: 50px;
display: flex;
border-radius: 60px;
padding: 5px;
font-size: 40px;
text-align: center;
align-content: center;
align-items: center;
justify-content: center;
}
#aqi-dot {
height: 80%;
aspect-ratio: 1 / 1;
border-radius: 50%;
background-color: var(--sub-text-color);
margin-right: 20px;
}
.main-icon {
width: 240px;
height: 240px;
}
#hours-info {
display: flex;
justify-content: space-between;
}
.hourly-item {
text-align: center;
background-color: #ffffff44;
border-radius: var(--sub-border-radius);
align-items: center;
padding: 20px 10px;
}
.hourly-icon{
width: 80%;
margin-bottom: 20px;
}
.hourly-temperature {
text-align: center;
color: var(--main-text-color);
font-size: 30px;
margin-bottom: 20px;
}
.hourly-time {
text-align: center;
color: var(--main-text-color);
font-size: 25px;
margin-bottom: 10px;
}
/**/
.daily-item {
display: flex;
position: relative;
justify-content: space-between;
align-items: center;
background-color: #ffffff44;
height: 90px;
border-radius: var(--sub-border-radius);
margin-bottom: 20px;
padding: 0 30px;
}
/*最后一个没有margin_button*/
.daily-item:last-child {
margin-bottom: 0;
}
.icon-day {
position: absolute;
left: 60%;
height: 80px;
}
.icon-night {
position: absolute;
left: 70%;
height: 80px;
}
.daily-weather{
position: absolute;
left: 30%;
}
.daily-temperature{
position: absolute;
left: 83%;
}
.daily-day, .daily-weather, .daily-temperature {
text-align: center;
color: var(--main-text-color);
font-size: 30px;
}

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

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