Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b42385f780 | |||
| 6cae38dea9 | |||
| 8594b59783 | |||
| f768c91430 | |||
| a65cb118cc | |||
| 75c6bbd23f | |||
| aaf0a75d65 | |||
| 8f560ce1ba | |||
| 9f3f79f51d | |||
| 92048aeff7 | |||
| 81aac10665 | |||
| 3ce230adfe | |||
| 4f885554ca | |||
| 7ebcb8add4 | |||
| e18cc82792 | |||
| eb28cd0a0c | |||
| 2d688a6ed6 | |||
| e9aac52200 | |||
| 4305548ab5 | |||
| 99382a3bf5 | |||
| 92e43785bf | |||
| fc5b11c5e8 | |||
| 0ec66988fa | |||
| e5c3081c22 | |||
| 14b356120a | |||
| a208302cb9 | |||
| 01ffa451bb | |||
| 2b6c2e84bd | |||
| 4f0a9af2dc | |||
| 4a4aa6b243 | |||
| 4c8625ae02 | |||
| c5f820a1f9 | |||
| a3dd2dbbda | |||
| 8d4f74dafe | |||
| 7c1bac64c9 | |||
| e09fa13d0f | |||
| 990a622cf6 | |||
| 6144563d4d |
@ -10,6 +10,10 @@ trigger:
|
||||
- master
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git submodule update --init --recursive
|
||||
- name: 构建 Docker 镜像
|
||||
image: plugins/docker:latest
|
||||
privileged: true
|
||||
@ -50,6 +54,10 @@ trigger:
|
||||
- tag
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git submodule update --init --recursive
|
||||
- name: 构建并推送 Release Docker 镜像
|
||||
image: plugins/docker:latest
|
||||
privileged: true
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "assets/lexicon/THUOCL"]
|
||||
path = assets/lexicon/THUOCL
|
||||
url = https://github.com/thunlp/THUOCL.git
|
||||
BIN
assets/img/dog/haoba_dog.jpg
Normal file
BIN
assets/img/dog/haoba_dog.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/img/meme/snaur_1_base.png
Executable file
BIN
assets/img/meme/snaur_1_base.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/img/meme/snaur_1_top.png
Executable file
BIN
assets/img/meme/snaur_1_top.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1008 KiB |
1
assets/json/poll.json
Normal file
1
assets/json/poll.json
Normal file
@ -0,0 +1 @@
|
||||
{"poll": {"0": {"create": 1760357553, "expiry": 1760443953, "options": {"0": "此方bot", "1": "testpilot", "2": "小镜bot", "3": "可怜bot"}, "polldata": {}, "qq": "2975499623", "title": "我~是~谁~?"}}}
|
||||
1
assets/lexicon/THUOCL
Submodule
1
assets/lexicon/THUOCL
Submodule
Submodule assets/lexicon/THUOCL added at a30ce79d89
1
assets/lexicon/ci.json
Normal file
1
assets/lexicon/ci.json
Normal file
File diff suppressed because one or more lines are too long
360393
assets/lexicon/common.txt
Normal file
360393
assets/lexicon/common.txt
Normal file
File diff suppressed because it is too large
Load Diff
339847
assets/lexicon/idiom.json
Normal file
339847
assets/lexicon/idiom.json
Normal file
File diff suppressed because it is too large
Load Diff
1
bot.py
1
bot.py
@ -42,6 +42,7 @@ def main():
|
||||
|
||||
# nonebot.load_builtin_plugin("echo")
|
||||
nonebot.load_plugins("konabot/plugins")
|
||||
nonebot.load_plugin("nonebot_plugin_analysis_bilibili")
|
||||
|
||||
nonebot.run()
|
||||
|
||||
|
||||
36
konabot/common/data_man.py
Normal file
36
konabot/common/data_man.py
Normal file
@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class DataManager(Generic[T]):
|
||||
def __init__(self, cls: type[T], fp: Path) -> None:
|
||||
self.cls = cls
|
||||
self.fp = fp
|
||||
self._aio_lock = asyncio.Lock()
|
||||
self._data: T | None = None
|
||||
|
||||
def load(self) -> T:
|
||||
if not self.fp.exists():
|
||||
return self.cls()
|
||||
try:
|
||||
return self.cls.model_validate_json(self.fp.read_text())
|
||||
except ValidationError:
|
||||
return self.cls()
|
||||
|
||||
def save(self, data: T):
|
||||
self.fp.write_text(data.model_dump_json())
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_data(self):
|
||||
await self._aio_lock.acquire()
|
||||
self._data = self.load()
|
||||
yield self._data
|
||||
self.save(self._data)
|
||||
self._data = None
|
||||
self._aio_lock.release()
|
||||
301
konabot/common/longtask.py
Normal file
301
konabot/common/longtask.py
Normal file
@ -0,0 +1,301 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
import datetime
|
||||
import json
|
||||
from typing import Annotated, Any, Callable, Coroutine, cast
|
||||
import asyncio as asynkio
|
||||
import uuid
|
||||
|
||||
from loguru import logger
|
||||
import nonebot
|
||||
from nonebot.params import Depends
|
||||
from nonebot.adapters import Event as BaseEvent
|
||||
from nonebot.adapters import Bot as BaseBot
|
||||
from nonebot.adapters.onebot.v11 import Bot as OBBot
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent as OBGroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11 import PrivateMessageEvent as OBPrivateMessageEvent
|
||||
from nonebot.adapters.console import Bot as ConsoleBot
|
||||
from nonebot.adapters.console import MessageEvent as ConsoleMessageEvent
|
||||
from nonebot.adapters.discord import MessageEvent as DCMessageEvent
|
||||
from nonebot.adapters.discord import Bot as DCBot
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from .path import DATA_PATH
|
||||
|
||||
LONGTASK_DATA_DIR = DATA_PATH / "longtasks.json"
|
||||
|
||||
|
||||
class LongTaskTarget(BaseModel):
|
||||
"""
|
||||
用于定义长期任务的目标沟通对象,一般通过 DepLongTaskTarget 依赖注入获取:
|
||||
|
||||
```python
|
||||
@cmd.handle()
|
||||
async def _(target: DepLongTaskTarget):
|
||||
...
|
||||
```
|
||||
"""
|
||||
|
||||
platform: str
|
||||
"沟通对象所在的平台"
|
||||
|
||||
self_id: str
|
||||
"进行沟通的对象自己的 ID"
|
||||
|
||||
channel_id: str
|
||||
"沟通对象所在的群或者 Discord Channel。若为空则代表是私聊"
|
||||
|
||||
target_id: str
|
||||
"沟通对象的 ID"
|
||||
|
||||
async def send_message(self, msg: UniMessage, at: bool = True) -> bool:
|
||||
try:
|
||||
bot = nonebot.get_bot(self.self_id)
|
||||
except KeyError:
|
||||
logger.warning(f"试图访问了不存在的 Bot。ID={self.self_id}")
|
||||
return False
|
||||
|
||||
if self.platform == "qq":
|
||||
if not isinstance(bot, OBBot):
|
||||
logger.warning(
|
||||
f"编号对应的平台并非期望的平台 ID={self.self_id} PLATFORM={
|
||||
self.platform
|
||||
} BOT_CLASS={bot.__class__.__name__}"
|
||||
)
|
||||
return False
|
||||
if self.channel_id == "":
|
||||
# 私聊模式
|
||||
await bot.send_private_msg(
|
||||
user_id=int(self.target_id),
|
||||
message=cast(Any, await msg.export(bot)),
|
||||
auto_escape=False,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
if at:
|
||||
msg = UniMessage().at(self.target_id).text(" ") + msg
|
||||
await bot.send_group_msg(
|
||||
group_id=int(self.channel_id),
|
||||
message=cast(Any, await msg.export(bot)),
|
||||
auto_escape=False,
|
||||
)
|
||||
return True
|
||||
if self.platform == "console":
|
||||
if not isinstance(bot, ConsoleBot):
|
||||
logger.warning(
|
||||
f"编号对应的平台并非期望的平台 ID={self.self_id} PLATFORM={
|
||||
self.platform
|
||||
} BOT_CLASS={bot.__class__.__name__}"
|
||||
)
|
||||
return False
|
||||
await bot.send_message(self.channel_id, cast(Any, await msg.export()))
|
||||
return True
|
||||
if self.platform == "discord":
|
||||
if not isinstance(bot, DCBot):
|
||||
logger.warning(
|
||||
f"编号对应的平台并非期望的平台 ID={self.self_id} PLATFORM={
|
||||
self.platform
|
||||
} BOT_CLASS={bot.__class__.__name__}"
|
||||
)
|
||||
return False
|
||||
await bot.send_to(
|
||||
channel_id=int(self.channel_id),
|
||||
message=cast(
|
||||
Any, await (UniMessage().at(self.target_id) + msg).export()
|
||||
),
|
||||
tts=False,
|
||||
)
|
||||
return True
|
||||
logger.warning(f"没有一个平台是期望的平台 PLATFORM={self.platform}")
|
||||
return False
|
||||
|
||||
|
||||
class LongTask(BaseModel):
|
||||
uuid: str
|
||||
data_json: str
|
||||
target: LongTaskTarget
|
||||
callback: str
|
||||
deadline: datetime.datetime
|
||||
canceled: bool = False
|
||||
|
||||
_aio_task: asynkio.Task | None = None
|
||||
|
||||
async def run(self):
|
||||
now = datetime.datetime.now()
|
||||
if self.deadline < now and not self.canceled:
|
||||
await self._run_task()
|
||||
return
|
||||
await asynkio.sleep((self.deadline - now).total_seconds())
|
||||
if self.canceled:
|
||||
return
|
||||
await self._run_task()
|
||||
|
||||
async def _run_task(self):
|
||||
hdl = registered_long_task_handler.get(self.callback, None)
|
||||
if hdl is None:
|
||||
logger.warning(
|
||||
f"Callback {self.callback} 未曾被注册,但是被期待调用,已忽略"
|
||||
)
|
||||
async with longtask_data() as datafile:
|
||||
datafile.to_handle[self.callback] = [
|
||||
t
|
||||
for t in datafile.to_handle.get(self.callback, [])
|
||||
if t.uuid != self.uuid
|
||||
]
|
||||
datafile.unhandled.setdefault(self.callback, []).append(self)
|
||||
|
||||
return
|
||||
success = False
|
||||
try:
|
||||
await hdl(self)
|
||||
success = True
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
async with longtask_data() as datafile:
|
||||
datafile.to_handle[self.callback] = [
|
||||
t for t in datafile.to_handle[self.callback] if t.uuid != self.uuid
|
||||
]
|
||||
if not success:
|
||||
datafile.unhandled.setdefault(self.callback, []).append(self)
|
||||
logger.info(
|
||||
f"LongTask 执行失败 UUID={self.uuid} callback={self.callback}"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"LongTask 工作完成 UUID={self.uuid} callback={self.callback}"
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
self._aio_task = None
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return json.loads(self.data_json)
|
||||
|
||||
async def start(self):
|
||||
self._aio_task = asynkio.Task(self.run())
|
||||
self._aio_task.add_done_callback(lambda _: self.clean())
|
||||
|
||||
|
||||
class LongTaskModuleData(BaseModel):
|
||||
to_handle: dict[str, list[LongTask]]
|
||||
unhandled: dict[str, list[LongTask]]
|
||||
|
||||
|
||||
async def get_long_task_target(event: BaseEvent, bot: BaseBot) -> LongTaskTarget | None:
|
||||
if isinstance(event, OBGroupMessageEvent):
|
||||
return LongTaskTarget(
|
||||
platform="qq",
|
||||
self_id=str(event.self_id),
|
||||
channel_id=str(event.group_id),
|
||||
target_id=str(event.user_id),
|
||||
)
|
||||
if isinstance(event, OBPrivateMessageEvent):
|
||||
return LongTaskTarget(
|
||||
platform="qq",
|
||||
self_id=str(event.self_id),
|
||||
channel_id="",
|
||||
target_id=str(event.user_id),
|
||||
)
|
||||
if isinstance(event, ConsoleMessageEvent):
|
||||
return LongTaskTarget(
|
||||
platform="console",
|
||||
self_id=str(event.self_id),
|
||||
channel_id=str(event.channel.id),
|
||||
target_id=str(event.user.id),
|
||||
)
|
||||
if isinstance(event, DCMessageEvent):
|
||||
self_id = ""
|
||||
if isinstance(bot, DCBot):
|
||||
self_id = str(bot.self_id)
|
||||
return LongTaskTarget(
|
||||
platform="discord",
|
||||
self_id=self_id,
|
||||
channel_id=str(event.channel_id),
|
||||
target_id=str(event.user_id),
|
||||
)
|
||||
|
||||
|
||||
_TaskHandler = Callable[[LongTask], Coroutine[Any, Any, Any]]
|
||||
|
||||
|
||||
registered_long_task_handler: dict[str, _TaskHandler] = {}
|
||||
longtask_lock = asynkio.Lock()
|
||||
|
||||
|
||||
def handle_long_task(callback_id: str):
|
||||
def _decorator(func: _TaskHandler):
|
||||
assert callback_id not in registered_long_task_handler, (
|
||||
"有长任务的 ID 出现冲突,请换个名字!"
|
||||
)
|
||||
registered_long_task_handler[callback_id] = func
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
def _load_longtask_data() -> LongTaskModuleData:
|
||||
try:
|
||||
txt = LONGTASK_DATA_DIR.read_text()
|
||||
return LongTaskModuleData.model_validate_json(txt)
|
||||
except (FileNotFoundError, ValidationError) as e:
|
||||
logger.info(f"取得 LongTask 数据时出现问题:{e}")
|
||||
return LongTaskModuleData(
|
||||
to_handle={},
|
||||
unhandled={},
|
||||
)
|
||||
|
||||
|
||||
def _save_longtask_data(data: LongTaskModuleData):
|
||||
LONGTASK_DATA_DIR.write_text(data.model_dump_json())
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def longtask_data():
|
||||
await longtask_lock.acquire()
|
||||
data = _load_longtask_data()
|
||||
yield data
|
||||
_save_longtask_data(data)
|
||||
longtask_lock.release()
|
||||
|
||||
|
||||
async def create_longtask(
|
||||
handler: str,
|
||||
data: dict[str, Any],
|
||||
target: LongTaskTarget,
|
||||
deadline: datetime.datetime,
|
||||
):
|
||||
task = LongTask(
|
||||
uuid=str(uuid.uuid4()),
|
||||
data_json=json.dumps(data),
|
||||
target=target,
|
||||
callback=handler,
|
||||
deadline=deadline,
|
||||
)
|
||||
|
||||
logger.info(f"创建了新的 LongTask UUID={task.uuid} CALLBACK={task.callback}")
|
||||
await task.start()
|
||||
|
||||
async with longtask_data() as d:
|
||||
d.to_handle.setdefault(handler, []).append(task)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
async def init_longtask():
|
||||
counter = 0
|
||||
req: set[str] = set()
|
||||
|
||||
async with longtask_data() as data:
|
||||
for v in data.to_handle.values():
|
||||
for t in v:
|
||||
await t.start()
|
||||
counter += 1
|
||||
req.add(t.callback)
|
||||
|
||||
logger.info(f"LongTask 启动了任务 数量={counter} 期望的门类=[{','.join(req)}]")
|
||||
|
||||
|
||||
DepLongTaskTarget = Annotated[LongTaskTarget, Depends(get_long_task_target)]
|
||||
34
konabot/common/nb/wzq_conflict.py
Normal file
34
konabot/common/nb/wzq_conflict.py
Normal file
@ -0,0 +1,34 @@
|
||||
from typing import cast
|
||||
from nonebot import get_bot, get_plugin_config, logger
|
||||
from nonebot.adapters import Event as BaseEvent
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11.bot import Bot as OnebotBot
|
||||
from nonebot.rule import Rule
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WZQConflictConfig(BaseModel):
|
||||
wzq_bot_qq: int = 0
|
||||
|
||||
config = get_plugin_config(WZQConflictConfig)
|
||||
|
||||
|
||||
async def no_wzqbot(evt: BaseEvent):
|
||||
if config.wzq_bot_qq <= 0:
|
||||
return True
|
||||
if not isinstance(evt, GroupMessageEvent):
|
||||
return True
|
||||
gid = evt.group_id
|
||||
sid = evt.self_id
|
||||
bot = cast(OnebotBot, get_bot(str(sid)))
|
||||
|
||||
members = await bot.get_group_member_list(group_id=gid)
|
||||
|
||||
members = set((m.get("user_id", -1) for m in members))
|
||||
if config.wzq_bot_qq in members:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
no_wzqbot_rule = Rule(no_wzqbot)
|
||||
|
||||
@ -12,3 +12,10 @@ DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
||||
DOCS_PATH_MAN3 = DOCS_PATH / "lib"
|
||||
DOCS_PATH_MAN7 = DOCS_PATH / "concepts"
|
||||
DOCS_PATH_MAN8 = DOCS_PATH / "sys"
|
||||
|
||||
if not DATA_PATH.exists():
|
||||
DATA_PATH.mkdir()
|
||||
|
||||
if not LOG_PATH.exists():
|
||||
LOG_PATH.mkdir()
|
||||
|
||||
|
||||
@ -5,17 +5,17 @@
|
||||
giftool [图片] [选项]
|
||||
|
||||
示例
|
||||
回复一张 GIF 并发送:
|
||||
`giftool --ss 1.5 -t 2.0`
|
||||
回复一张 GIF 并发送:
|
||||
`giftool --ss 1.5 -t 2.0`
|
||||
从 1.5 秒处开始,截取 2 秒长度的片段。
|
||||
|
||||
`giftool [图片] --ss 0:10 -to 0:15`
|
||||
`giftool [图片] --ss 0:10 -to 0:15`
|
||||
截取从 10 秒到 15 秒之间的片段(支持 MM:SS 或 HH:MM:SS 格式)。
|
||||
|
||||
`giftool [图片] --frames:v 10`
|
||||
`giftool [图片] --frames:v 10`
|
||||
将整张 GIF 均匀抽帧,最终保留 10 帧。
|
||||
|
||||
`giftool [图片] --ss 2 --frames:v 5`
|
||||
`giftool [图片] --ss 2 --frames:v 5`
|
||||
从第 2 秒开始截取,并将结果抽帧为 5 帧。
|
||||
|
||||
参数说明
|
||||
@ -45,8 +45,8 @@
|
||||
- 帧数必须为正整数(> 0)。
|
||||
- 若原始帧数 ≤ 指定帧数,则保留全部帧。
|
||||
|
||||
--s <速度>(可选)
|
||||
- 调整 gif 图的速度
|
||||
--speed <速度>(可选)
|
||||
- 调整 gif 图的速度。若为负数,则代表倒放
|
||||
|
||||
使用方式
|
||||
1. 发送指令前,请确保:
|
||||
|
||||
20
konabot/docs/user/卵总展示.txt
Normal file
20
konabot/docs/user/卵总展示.txt
Normal file
@ -0,0 +1,20 @@
|
||||
指令介绍
|
||||
卵总展示 - 让卵总举起你的图片
|
||||
|
||||
格式
|
||||
<引用图片> 卵总展示 [选项]
|
||||
卵总展示 [选项] <图片>
|
||||
|
||||
选项
|
||||
`--whiteness <number>` 白度
|
||||
将原图进行指数变换,以调整它的白的程度,默认为 0.0
|
||||
|
||||
`--black-level <number>` 黑色等级
|
||||
将原图减淡,数值越大越淡,范围 0.0-1.0,默认 0.2
|
||||
|
||||
`--opacity <number>` 不透明度
|
||||
将你的图片叠放在图片上的不透明度,默认为 0.8
|
||||
|
||||
`--saturation <number>` 饱和度
|
||||
调整原图的饱和度,应该要大于 0.0,默认为 0.85
|
||||
|
||||
11
konabot/docs/user/发起投票.txt
Normal file
11
konabot/docs/user/发起投票.txt
Normal file
@ -0,0 +1,11 @@
|
||||
指令介绍
|
||||
发起投票 - 发起一个投票
|
||||
|
||||
格式
|
||||
发起投票 <投票标题> <选项1> <选项2> ...
|
||||
|
||||
示例
|
||||
`发起投票 这是一个投票 A B C` 发起标题为“这是一个投票”,选项为“A”、“B”、“C”的投票
|
||||
|
||||
说明
|
||||
投票各个选项之间用空格分隔,选项数量为2-15项。投票的默认有效期为24小时。
|
||||
12
konabot/docs/user/投票.txt
Normal file
12
konabot/docs/user/投票.txt
Normal file
@ -0,0 +1,12 @@
|
||||
指令介绍
|
||||
投票 - 参与已发起的投票
|
||||
|
||||
格式
|
||||
投票 <投票ID/标题> <选项文本>
|
||||
|
||||
示例
|
||||
`投票 1 A` 在ID为1的投票中,投给“A”
|
||||
`投票 这是一个投票 B` 在标题为“这是一个投票”的投票中,投给“B”
|
||||
|
||||
说明
|
||||
目前不支持单人多投,每个人只能投一项。
|
||||
12
konabot/docs/user/查看投票.txt
Normal file
12
konabot/docs/user/查看投票.txt
Normal file
@ -0,0 +1,12 @@
|
||||
指令介绍
|
||||
查看投票 - 查看已发起的投票
|
||||
|
||||
格式
|
||||
查看投票 <投票ID或标题>
|
||||
|
||||
示例
|
||||
`查看投票 1` 查看ID为1的投票
|
||||
`查看投票 这是一个投票` 查看标题为“这是一个投票”的投票
|
||||
|
||||
说明
|
||||
投票在进行时,使用此命令可以看到投票的各个选项;投票结束后,则可以看到各项的票数。
|
||||
8
konabot/docs/user/生成二维码.txt
Normal file
8
konabot/docs/user/生成二维码.txt
Normal file
@ -0,0 +1,8 @@
|
||||
指令介绍
|
||||
生成二维码 - 将文本内容转换为二维码
|
||||
|
||||
格式
|
||||
生成二维码 <文本内容>
|
||||
|
||||
示例
|
||||
`生成二维码 嗨嗨嗨` 生成扫描结果为“嗨嗨嗨”的二维码图片
|
||||
25
konabot/plugins/auto_accept.py
Normal file
25
konabot/plugins/auto_accept.py
Normal file
@ -0,0 +1,25 @@
|
||||
import asyncio
|
||||
import random
|
||||
from typing import cast
|
||||
|
||||
from loguru import logger
|
||||
from nonebot import get_bot, on_request
|
||||
from nonebot.adapters.onebot.v11.event import FriendRequestEvent
|
||||
from nonebot.adapters.onebot.v11.bot import Bot as OnebotBot
|
||||
|
||||
from konabot.common.nb.is_admin import cfg as adminConfig
|
||||
|
||||
add_request = on_request()
|
||||
|
||||
@add_request.handle()
|
||||
async def _(req: FriendRequestEvent):
|
||||
bot = cast(OnebotBot, get_bot(str(req.self_id)))
|
||||
ok_member_ls: set[int] = set()
|
||||
for group in adminConfig.admin_qq_group:
|
||||
members = await bot.get_group_member_list(group_id=group)
|
||||
ok_member_ls |= cast(set[int], set((m.get("user_id") for m in members)))
|
||||
if req.user_id in ok_member_ls:
|
||||
await asyncio.sleep(random.randint(5, 10))
|
||||
await req.approve(bot)
|
||||
logger.info(f"已经自动同意 {req.user_id} 的好友请求")
|
||||
|
||||
39
konabot/plugins/bilibili_fetch/__init__.py
Normal file
39
konabot/plugins/bilibili_fetch/__init__.py
Normal file
@ -0,0 +1,39 @@
|
||||
import re
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot_plugin_alconna import Reference, Reply, UniMsg
|
||||
|
||||
from nonebot.adapters import Event
|
||||
|
||||
|
||||
matcher_fix = on_message()
|
||||
|
||||
pattern = (
|
||||
r"^(?:(?:av|cv)\d+|BV[a-zA-Z0-9]{10})|"
|
||||
r"(?:b23\.tv|bili(?:22|23|33|2233)\.cn|\.bilibili\.com|QQ小程序(?:&#93;|]|\])哔哩哔哩).{0,500}"
|
||||
)
|
||||
|
||||
|
||||
@matcher_fix.handle()
|
||||
async def _(msg: UniMsg, event: Event):
|
||||
to_search = msg.exclude(Reply, Reference).dump(json=True)
|
||||
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
|
||||
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
|
||||
return
|
||||
|
||||
from nonebot_plugin_analysis_bilibili import handle_analysis
|
||||
|
||||
await handle_analysis(event)
|
||||
|
||||
# b_url: str
|
||||
# b_page: str | None
|
||||
# b_time: str | None
|
||||
#
|
||||
# from nonebot_plugin_analysis_bilibili.analysis_bilibili import extract as bilibili_extract
|
||||
#
|
||||
# b_url, b_page, b_time = bilibili_extract(to_search)
|
||||
# if b_url is None:
|
||||
# return
|
||||
#
|
||||
# await matcher_fix.send(await UniMessage().text(b_url).export())
|
||||
|
||||
71
konabot/plugins/gen_qrcode/__init__.py
Normal file
71
konabot/plugins/gen_qrcode/__init__.py
Normal file
@ -0,0 +1,71 @@
|
||||
import qrcode
|
||||
# from pyzbar.pyzbar import decode
|
||||
# from PIL import Image
|
||||
import requests
|
||||
from io import BytesIO
|
||||
|
||||
from nonebot_plugin_alconna import (Alconna, Args, Field, MultiVar, UniMessage,
|
||||
on_alconna)
|
||||
from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
|
||||
|
||||
from konabot.common.nb.wzq_conflict import no_wzqbot_rule
|
||||
|
||||
async def download_img(url):
|
||||
resp = requests.get(url.replace("https://multimedia.nt.qq","http://multimedia.nt.qq")) # bim获取QQ的图片时避免SSLv3报错
|
||||
img_bytes = BytesIO()
|
||||
with open(img_bytes,"wb") as f:
|
||||
f.write(resp.content)
|
||||
return img_bytes
|
||||
|
||||
def genqr(data):
|
||||
qr = qrcode.QRCode(version=1,error_correction=qrcode.constants.ERROR_CORRECT_L,box_size=8,border=4)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format="PNG")
|
||||
return img_bytes
|
||||
|
||||
"""
|
||||
async def recqr(url):
|
||||
im_path = "assets/img/qrcode/2.jpg"
|
||||
data = await download_img(url)
|
||||
img = Image.open(im_path)
|
||||
decoded_objects = decode(img)
|
||||
data = ""
|
||||
for obj in decoded_objects:
|
||||
data += obj.data.decode('utf-8')
|
||||
return data
|
||||
"""
|
||||
|
||||
gqrc = on_alconna(Alconna(
|
||||
"genqr",
|
||||
Args["saying", MultiVar(str, '+'), Field(
|
||||
missing_tips=lambda: "请输入你要转换为二维码的文字!"
|
||||
)],
|
||||
# UniMessage[]
|
||||
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"生成二维码","genqrcode"}, rule=no_wzqbot_rule)
|
||||
|
||||
@gqrc.handle()
|
||||
async def _(saying: list):
|
||||
"""
|
||||
img = await draw_pt("\n".join(saying))
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format="PNG")
|
||||
|
||||
await pt.send(await UniMessage().image(raw=img_bytes).export())
|
||||
|
||||
# print(saying)
|
||||
# 二维码识别
|
||||
if type(saying[0]) == 'image':
|
||||
data = await recqr(saying[0].data['url'])
|
||||
if data == "":
|
||||
await gqrc.send("二维码图片解析失败!")
|
||||
else:
|
||||
await gqrc.send(recqr(saying[0].data['url']))
|
||||
|
||||
# 二维码生成
|
||||
else:
|
||||
"""
|
||||
# genqr("\n".join(saying))
|
||||
await gqrc.send(await UniMessage().image(raw=genqr("\n".join(saying))).export())
|
||||
518
konabot/plugins/idiomgame/__init__.py
Normal file
518
konabot/plugins/idiomgame/__init__.py
Normal file
@ -0,0 +1,518 @@
|
||||
import asyncio as asynkio
|
||||
import datetime
|
||||
import json
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Event as BaseEvent
|
||||
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
UniMessage,
|
||||
UniMsg,
|
||||
on_alconna,
|
||||
)
|
||||
|
||||
from konabot.common.longtask import DepLongTaskTarget
|
||||
from konabot.common.path import ASSETS_PATH
|
||||
|
||||
DATA_FILE_PATH = (
|
||||
Path(__file__).parent.parent.parent.parent / "data" / "idiom_banned.json"
|
||||
)
|
||||
|
||||
|
||||
def load_banned_ids() -> list[str]:
|
||||
if not DATA_FILE_PATH.exists():
|
||||
return []
|
||||
try:
|
||||
return json.loads(DATA_FILE_PATH.read_text())
|
||||
except Exception as e:
|
||||
logger.warning(f"在解析成语接龙封禁文件时遇到问题:{e}")
|
||||
return []
|
||||
|
||||
|
||||
def is_idiom_game_banned(group_id: str) -> bool:
|
||||
banned_ids = load_banned_ids()
|
||||
return group_id in banned_ids
|
||||
|
||||
|
||||
def add_banned_id(group_id: str):
|
||||
banned_ids = load_banned_ids()
|
||||
if group_id not in banned_ids:
|
||||
banned_ids.append(group_id)
|
||||
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4))
|
||||
|
||||
|
||||
def remove_banned_id(group_id: str):
|
||||
banned_ids = load_banned_ids()
|
||||
if group_id in banned_ids:
|
||||
banned_ids.remove(group_id)
|
||||
DATA_FILE_PATH.write_text(json.dumps(banned_ids, ensure_ascii=False, indent=4))
|
||||
|
||||
|
||||
class TryStartState(Enum):
|
||||
STARTED = 0
|
||||
ALREADY_PLAYING = 1
|
||||
NO_REMAINING_TIMES = 2
|
||||
|
||||
|
||||
class TryStopState(Enum):
|
||||
STOPPED = 0
|
||||
NOT_PLAYING = 1
|
||||
|
||||
|
||||
class TryVerifyState(Enum):
|
||||
VERIFIED = 0
|
||||
NOT_IDIOM = 1
|
||||
WRONG_FIRST_CHAR = 2
|
||||
VERIFIED_BUT_NO_NEXT = 3
|
||||
VERIFIED_GAME_END = 4
|
||||
|
||||
|
||||
class IdiomGame:
|
||||
ALL_WORDS = [] # 所有四字词语
|
||||
ALL_IDIOMS = [] # 所有成语
|
||||
INSTANCE_LIST: dict[str, "IdiomGame"] = {} # 群号对应的游戏实例
|
||||
IDIOM_FIRST_CHAR = {} # 成语首字字典
|
||||
|
||||
__inited = False
|
||||
|
||||
def __init__(self, group_id: str):
|
||||
# 初始化一局游戏
|
||||
self.group_id = ""
|
||||
self.now_playing = False
|
||||
self.score_board = {}
|
||||
self.last_idiom = ""
|
||||
self.last_char = ""
|
||||
self.remain_playing_times = 3
|
||||
self.last_play_date = ""
|
||||
self.all_buff_score = 0
|
||||
self.lock = asynkio.Lock()
|
||||
self.remain_rounds = 0 # 剩余回合数
|
||||
IdiomGame.INSTANCE_LIST[group_id] = self
|
||||
|
||||
def be_able_to_play(self) -> bool:
|
||||
if self.last_play_date != datetime.date.today():
|
||||
self.last_play_date = datetime.date.today()
|
||||
self.remain_playing_times = 1
|
||||
if self.remain_playing_times > 0:
|
||||
self.remain_playing_times -= 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def choose_start_idiom(self) -> str:
|
||||
"""
|
||||
随机选择一个成语作为起始成语
|
||||
"""
|
||||
self.last_idiom = secrets.choice(IdiomGame.ALL_IDIOMS)
|
||||
self.last_char = self.last_idiom[-1]
|
||||
if not self.is_nextable(self.last_char):
|
||||
self.choose_start_idiom()
|
||||
return self.last_idiom
|
||||
|
||||
@classmethod
|
||||
def try_start_game(cls, group_id: str, force: bool = False) -> TryStartState:
|
||||
cls.init_lexicon()
|
||||
if not cls.INSTANCE_LIST.get(group_id):
|
||||
cls(group_id)
|
||||
instance = cls.INSTANCE_LIST[group_id]
|
||||
if instance.now_playing:
|
||||
return TryStartState.ALREADY_PLAYING
|
||||
if not instance.be_able_to_play() and not force:
|
||||
return TryStartState.NO_REMAINING_TIMES
|
||||
instance.now_playing = True
|
||||
return TryStartState.STARTED
|
||||
|
||||
def start_game(self, rounds: int = 100):
|
||||
self.now_playing = True
|
||||
self.remain_rounds = rounds
|
||||
self.choose_start_idiom()
|
||||
|
||||
@classmethod
|
||||
def try_stop_game(cls, group_id: str) -> TryStopState:
|
||||
if not cls.INSTANCE_LIST.get(group_id):
|
||||
return TryStopState.NOT_PLAYING
|
||||
instance = cls.INSTANCE_LIST[group_id]
|
||||
if not instance.now_playing:
|
||||
return TryStopState.NOT_PLAYING
|
||||
instance.now_playing = False
|
||||
return TryStopState.STOPPED
|
||||
|
||||
def clear_score_board(self):
|
||||
self.score_board = {}
|
||||
self.last_char = ""
|
||||
|
||||
def get_score_board(self) -> dict:
|
||||
return self.score_board
|
||||
|
||||
def get_all_buff_score(self) -> int:
|
||||
return self.all_buff_score
|
||||
|
||||
async def skip_idiom(self, buff_score: int = -100) -> str:
|
||||
"""
|
||||
跳过当前成语,选择下一个成语
|
||||
"""
|
||||
await self.lock.acquire()
|
||||
self._skip_idiom_async(buff_score)
|
||||
self.lock.release()
|
||||
return self.last_idiom
|
||||
|
||||
def _skip_idiom_async(self, buff_score: int = -100) -> str:
|
||||
self.last_idiom = secrets.choice(IdiomGame.ALL_IDIOMS)
|
||||
self.last_char = self.last_idiom[-1]
|
||||
self.add_buff_score(buff_score)
|
||||
return self.last_idiom
|
||||
|
||||
async def try_verify_idiom(self, idiom: str, user_id: str) -> TryVerifyState:
|
||||
"""
|
||||
用户发送成语
|
||||
"""
|
||||
await self.lock.acquire()
|
||||
state = self._verify_idiom(idiom, user_id)
|
||||
self.lock.release()
|
||||
return state
|
||||
|
||||
def is_nextable(self, last_char: str) -> bool:
|
||||
"""
|
||||
判断是否有成语可以接
|
||||
"""
|
||||
return last_char in IdiomGame.IDIOM_FIRST_CHAR
|
||||
|
||||
def _verify_idiom(self, idiom: str, user_id: str) -> TryVerifyState:
|
||||
# 新成语的首字应与上一条成语的尾字相同
|
||||
if idiom[0] != self.last_char:
|
||||
return TryVerifyState.WRONG_FIRST_CHAR
|
||||
if idiom not in IdiomGame.ALL_IDIOMS and idiom not in IdiomGame.ALL_WORDS:
|
||||
self.add_score(user_id, -0.1)
|
||||
return TryVerifyState.NOT_IDIOM
|
||||
self.last_idiom = idiom
|
||||
self.last_char = idiom[-1]
|
||||
self.add_score(user_id, 1)
|
||||
self.remain_rounds -= 1
|
||||
if self.remain_rounds <= 0:
|
||||
self.now_playing = False
|
||||
return TryVerifyState.VERIFIED_GAME_END
|
||||
if not self.is_nextable(self.last_char):
|
||||
# 没有成语可以接了,自动跳过
|
||||
self._skip_idiom_async()
|
||||
return TryVerifyState.VERIFIED_BUT_NO_NEXT
|
||||
return TryVerifyState.VERIFIED
|
||||
|
||||
def get_user_score(self, user_id: str) -> int:
|
||||
if user_id not in self.score_board:
|
||||
return 0
|
||||
return self.score_board[user_id]["score"]
|
||||
|
||||
def add_score(self, user_id: str, score: int):
|
||||
if user_id not in self.score_board:
|
||||
self.score_board[user_id] = {"name": user_id, "score": 0}
|
||||
self.score_board[user_id]["score"] += score
|
||||
|
||||
def add_buff_score(self, score: int):
|
||||
self.all_buff_score += score
|
||||
|
||||
def get_playing_state(self) -> bool:
|
||||
return self.now_playing
|
||||
|
||||
def get_last_char(self) -> str:
|
||||
return self.last_char
|
||||
|
||||
@classmethod
|
||||
def init_lexicon(cls):
|
||||
if cls.__inited:
|
||||
return
|
||||
cls.__inited = True
|
||||
|
||||
# 成语大表
|
||||
with open(ASSETS_PATH / "lexicon" / "idiom.json", "r", encoding="utf-8") as f:
|
||||
ALL_IDIOMS_INFOS = json.load(f)
|
||||
|
||||
# 词语大表
|
||||
with open(ASSETS_PATH / "lexicon" / "ci.json", "r", encoding="utf-8") as f:
|
||||
cls.ALL_WORDS = json.load(f)
|
||||
|
||||
COMMON_WORDS = []
|
||||
# 读取 COMMON 词语大表
|
||||
with open(ASSETS_PATH / "lexicon" / "common.txt", "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
word = line.strip()
|
||||
if len(word) == 4:
|
||||
COMMON_WORDS.append(word)
|
||||
|
||||
# 读取 THUOCL 成语库
|
||||
with open(
|
||||
ASSETS_PATH / "lexicon" / "THUOCL" / "data" / "THUOCL_chengyu.txt",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
THUOCL_IDIOMS = [line.split(" ")[0].strip() for line in f]
|
||||
|
||||
# 读取 THUOCL 剩下的所有 txt 文件,只保留四字词
|
||||
THUOCL_WORDS = []
|
||||
import os
|
||||
|
||||
for filename in os.listdir(ASSETS_PATH / "lexicon" / "THUOCL" / "data"):
|
||||
if filename.endswith(".txt") and filename != "THUOCL_chengyu.txt":
|
||||
with open(
|
||||
ASSETS_PATH / "lexicon" / "THUOCL" / "data" / filename,
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
for line in f:
|
||||
word = line.lstrip().split(" ")[0].strip()
|
||||
if len(word) == 4:
|
||||
THUOCL_WORDS.append(word)
|
||||
|
||||
# 只有成语的大表
|
||||
cls.ALL_IDIOMS = [idiom["word"] for idiom in ALL_IDIOMS_INFOS] + THUOCL_IDIOMS
|
||||
cls.ALL_IDIOMS = list(set(cls.ALL_IDIOMS)) # 去重
|
||||
|
||||
# 其他四字词语表,仅表示可以有这个词
|
||||
cls.ALL_WORDS = (
|
||||
[word for word in cls.ALL_WORDS if len(word) == 4]
|
||||
+ THUOCL_WORDS
|
||||
+ COMMON_WORDS
|
||||
)
|
||||
cls.ALL_WORDS = list(set(cls.ALL_WORDS)) # 去重
|
||||
|
||||
# 根据成语大表,划分出成语首字字典
|
||||
for idiom in cls.ALL_IDIOMS + cls.ALL_WORDS:
|
||||
if idiom[0] not in cls.IDIOM_FIRST_CHAR:
|
||||
cls.IDIOM_FIRST_CHAR[idiom[0]] = []
|
||||
cls.IDIOM_FIRST_CHAR[idiom[0]].append(idiom)
|
||||
|
||||
|
||||
evt = on_alconna(
|
||||
Alconna(
|
||||
"我要玩成语接龙",
|
||||
Args["rounds?", int],
|
||||
),
|
||||
use_cmd_start=True,
|
||||
use_cmd_sep=False,
|
||||
skip_for_unmatch=True,
|
||||
)
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def play_game(
|
||||
event: BaseEvent,
|
||||
target: DepLongTaskTarget,
|
||||
force=False,
|
||||
rounds: Optional[int] = 100,
|
||||
):
|
||||
# group_id = str(event.get_session_id())
|
||||
group_id = target.channel_id
|
||||
if is_idiom_game_banned(group_id):
|
||||
await evt.send(
|
||||
await UniMessage().text("本群已被禁止使用成语接龙功能!").export()
|
||||
)
|
||||
return
|
||||
rounds = rounds or 0
|
||||
if rounds <= 0:
|
||||
await evt.send(await UniMessage().text("干什么!你想玩负数局吗?").export())
|
||||
return
|
||||
state = IdiomGame.try_start_game(group_id, force)
|
||||
if state == TryStartState.ALREADY_PLAYING:
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.text("当前已有成语接龙游戏在进行中,请稍后再试!")
|
||||
.export()
|
||||
)
|
||||
return
|
||||
if state == TryStartState.NO_REMAINING_TIMES:
|
||||
await evt.send(await UniMessage().text("玩玩玩,就知道玩,快去睡觉!").export())
|
||||
return
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.text(
|
||||
"你小子,还真有意思!\n好,成语接龙游戏开始!我说一个成语,请大家接下去!"
|
||||
)
|
||||
.export()
|
||||
)
|
||||
instance = IdiomGame.INSTANCE_LIST[group_id]
|
||||
instance.start_game(rounds)
|
||||
# 发布成语
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.text(f"第一个成语:「{instance.last_idiom}」,请接!")
|
||||
.export()
|
||||
)
|
||||
|
||||
|
||||
evt = on_alconna(
|
||||
Alconna(
|
||||
"老子就是要玩成语接龙!!!",
|
||||
Args["rounds?", int],
|
||||
),
|
||||
use_cmd_start=True,
|
||||
use_cmd_sep=False,
|
||||
skip_for_unmatch=True,
|
||||
)
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def force_play_game(
|
||||
event: BaseEvent, target: DepLongTaskTarget, rounds: Optional[int] = 100
|
||||
):
|
||||
await play_game(event, target, force=True, rounds=rounds)
|
||||
|
||||
|
||||
async def end_game(event: BaseEvent, group_id: str):
|
||||
instance = IdiomGame.INSTANCE_LIST[group_id]
|
||||
result_text = UniMessage().text("游戏结束!\n最终得分榜:\n")
|
||||
score_board = instance.get_score_board()
|
||||
if len(score_board) == 0:
|
||||
result_text += "无人得分!"
|
||||
else:
|
||||
# 按分数排序,名字用 at 的方式
|
||||
sorted_score = sorted(
|
||||
score_board.items(), key=lambda x: x[1]["score"], reverse=True
|
||||
)
|
||||
for i, (user_id, info) in enumerate(sorted_score):
|
||||
result_text += (
|
||||
f"{i + 1}. "
|
||||
+ UniMessage().at(user_id)
|
||||
+ f": {info['score'] + instance.get_all_buff_score()} 分\n"
|
||||
)
|
||||
await evt.send(await result_text.export())
|
||||
instance.clear_score_board()
|
||||
|
||||
|
||||
evt = on_alconna(
|
||||
Alconna("不玩了"), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
|
||||
)
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||
# group_id = str(event.get_session_id())
|
||||
group_id = target.channel_id
|
||||
state = IdiomGame.try_stop_game(group_id)
|
||||
if state == TryStopState.STOPPED:
|
||||
# 发送好吧狗图片
|
||||
# 打开好吧狗本地文件
|
||||
with open(ASSETS_PATH / "img" / "dog" / "haoba_dog.jpg", "rb") as f:
|
||||
img_data = f.read()
|
||||
await evt.send(await UniMessage().image(raw=img_data).export())
|
||||
await end_game(event, group_id)
|
||||
else:
|
||||
await evt.send(
|
||||
await UniMessage().text("当前没有成语接龙游戏在进行中!").export()
|
||||
)
|
||||
|
||||
|
||||
# 跳过
|
||||
evt = on_alconna(
|
||||
Alconna("跳过成语"), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
|
||||
)
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def _(target: DepLongTaskTarget):
|
||||
# group_id = str(event.get_session_id())
|
||||
group_id = target.channel_id
|
||||
instance = IdiomGame.INSTANCE_LIST.get(group_id)
|
||||
if not instance or not instance.get_playing_state():
|
||||
return
|
||||
await evt.send(await UniMessage().text("你们太菜了!全部扣100分!").export())
|
||||
idiom = await instance.skip_idiom(-100)
|
||||
await evt.send(
|
||||
await UniMessage().text(f"重新开始,下一个成语是「{idiom}」").export()
|
||||
)
|
||||
|
||||
|
||||
def get_user_info(event: BaseEvent):
|
||||
if isinstance(event, DiscordMessageEvent):
|
||||
user_id = str(event.author.id)
|
||||
user_name = str(event.author.name)
|
||||
else:
|
||||
user_id = str(event.get_user_id())
|
||||
user_name = str(event.get_user_id())
|
||||
return user_id, user_name
|
||||
|
||||
|
||||
# 直接读取消息
|
||||
evt = on_message()
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def _(event: BaseEvent, msg: UniMsg, target: DepLongTaskTarget):
|
||||
# group_id = str(event.get_session_id())
|
||||
group_id = target.channel_id
|
||||
instance = IdiomGame.INSTANCE_LIST.get(group_id)
|
||||
if not instance or not instance.get_playing_state():
|
||||
return
|
||||
user_idiom = msg.extract_plain_text().strip()
|
||||
user_id, user_name = get_user_info(event)
|
||||
state = await instance.try_verify_idiom(user_idiom, user_id)
|
||||
if state == TryVerifyState.WRONG_FIRST_CHAR:
|
||||
return
|
||||
if state == TryVerifyState.NOT_IDIOM:
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.at(user_id)
|
||||
.text("接不上!这个不一样!你被扣了 0.1 分!")
|
||||
.export()
|
||||
)
|
||||
return
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.at(user_id)
|
||||
.text(f"接对了!你有 {instance.get_user_score(user_id)} 分!")
|
||||
.export()
|
||||
)
|
||||
if state == TryVerifyState.VERIFIED_GAME_END:
|
||||
await evt.send(await UniMessage().text("全部回合结束!").export())
|
||||
await end_game(event, group_id)
|
||||
return
|
||||
if state == TryVerifyState.VERIFIED_BUT_NO_NEXT:
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.text("但是,这是条死路!你们全部都要扣 100 分!")
|
||||
.export()
|
||||
)
|
||||
await evt.send(
|
||||
await UniMessage().text(f"重新抽取成语「{instance.last_idiom}」").export()
|
||||
)
|
||||
await evt.send(
|
||||
await UniMessage()
|
||||
.text(f"下一个成语请以「{instance.get_last_char()}」开头!")
|
||||
.export()
|
||||
)
|
||||
|
||||
|
||||
evt = on_alconna(
|
||||
Alconna("禁止成语接龙"),
|
||||
use_cmd_start=True,
|
||||
use_cmd_sep=False,
|
||||
skip_for_unmatch=True,
|
||||
)
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||
# group_id = str(event.get_session_id())
|
||||
group_id = target.channel_id
|
||||
add_banned_id(group_id)
|
||||
await evt.send(await UniMessage().text("本群已被禁止使用成语接龙功能!").export())
|
||||
|
||||
|
||||
evt = on_alconna(
|
||||
Alconna("开启成语接龙"),
|
||||
use_cmd_start=True,
|
||||
use_cmd_sep=False,
|
||||
skip_for_unmatch=True,
|
||||
)
|
||||
|
||||
|
||||
@evt.handle()
|
||||
async def _(event: BaseEvent, target: DepLongTaskTarget):
|
||||
# group_id = str(event.get_session_id())
|
||||
group_id = target.channel_id
|
||||
remove_banned_id(group_id)
|
||||
await evt.send(await UniMessage().text("本群已开启成语接龙功能!").export())
|
||||
3
konabot/plugins/idiomgame/base/path.py
Normal file
3
konabot/plugins/idiomgame/base/path.py
Normal file
@ -0,0 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
ASSETS = Path(__file__).parent.parent / "assets"
|
||||
@ -71,8 +71,11 @@ async def _(
|
||||
raise BotExceptionMessage("错误:出点时间小于入点")
|
||||
if frame_count is not None and frame_count <= 0:
|
||||
raise BotExceptionMessage("错误:帧数量应该大于 0")
|
||||
if speed_factor <= 0:
|
||||
raise BotExceptionMessage("错误:--speed 必须大于 0")
|
||||
if speed_factor == 0:
|
||||
raise BotExceptionMessage("错误:速度不能为 0")
|
||||
|
||||
is_rev = speed_factor < 0
|
||||
speed_factor = abs(speed_factor)
|
||||
|
||||
if not getattr(image, "is_animated", False):
|
||||
raise BotExceptionMessage("错误:输入的不是动图(GIF)")
|
||||
@ -185,6 +188,10 @@ async def _(
|
||||
if transparency_flag:
|
||||
tf['transparency'] = 0
|
||||
|
||||
if is_rev:
|
||||
rframes = rframes[::-1]
|
||||
rdur = rdur[::-1]
|
||||
|
||||
if rframes:
|
||||
rframes[0].save(
|
||||
output_img,
|
||||
|
||||
47
konabot/plugins/longtask_core.py
Normal file
47
konabot/plugins/longtask_core.py
Normal file
@ -0,0 +1,47 @@
|
||||
import asyncio
|
||||
|
||||
# import datetime
|
||||
from loguru import logger
|
||||
import nonebot
|
||||
|
||||
# from nonebot_plugin_alconna import UniMessage
|
||||
from konabot.common.longtask import (
|
||||
# DepLongTaskTarget,
|
||||
# LongTask,
|
||||
# create_longtask,
|
||||
# handle_long_task,
|
||||
init_longtask,
|
||||
)
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
INIT_FLAG = {"flag": False}
|
||||
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def _():
|
||||
if INIT_FLAG["flag"]:
|
||||
return
|
||||
INIT_FLAG["flag"] = True
|
||||
logger.info("有 Bot 连接,等待 5 秒后初始化 LongTask 模块")
|
||||
await asyncio.sleep(5)
|
||||
await init_longtask()
|
||||
logger.info("LongTask 初始化完成")
|
||||
|
||||
|
||||
# cmd1 = nonebot.on_command("test114")
|
||||
#
|
||||
#
|
||||
# @handle_long_task("test_callback_001")
|
||||
# async def _(lt: LongTask):
|
||||
# await lt.target.send_message(UniMessage().text("Hello, world!"), at=True)
|
||||
#
|
||||
#
|
||||
# @cmd1.handle()
|
||||
# async def _(target: DepLongTaskTarget):
|
||||
# await create_longtask(
|
||||
# handler="test_callback_001",
|
||||
# data={},
|
||||
# target=target,
|
||||
# deadline=datetime.datetime.now() + datetime.timedelta(seconds=20),
|
||||
# )
|
||||
@ -2,11 +2,11 @@ from io import BytesIO
|
||||
from typing import Iterable, cast
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot_plugin_alconna import (Alconna, Args, Field, MultiVar, Text,
|
||||
from nonebot_plugin_alconna import (Alconna, Args, Field, Image, MultiVar, Option, Text,
|
||||
UniMessage, UniMsg, on_alconna)
|
||||
|
||||
from konabot.common.nb.extract_image import extract_image_from_message
|
||||
from konabot.plugins.memepack.drawing.display import draw_cao_display
|
||||
from konabot.common.nb.extract_image import PIL_Image, extract_image_from_message
|
||||
from konabot.plugins.memepack.drawing.display import draw_cao_display, draw_snaur_display
|
||||
from konabot.plugins.memepack.drawing.saying import (draw_cute_ten,
|
||||
draw_geimao, draw_mnk,
|
||||
draw_pt, draw_suan)
|
||||
@ -139,3 +139,24 @@ async def _(msg: UniMsg, evt: Event, bot: Bot):
|
||||
.text(err)
|
||||
.export()
|
||||
)
|
||||
|
||||
|
||||
snaur_display_cmd = on_alconna(Alconna(
|
||||
"卵总展示",
|
||||
Option("--whiteness", Args["whiteness", float], alias=["-w"]),
|
||||
Option("--black-level", Args["black_level", float], alias=["-b"]),
|
||||
Option("--opacity", Args["opacity", float], alias=["-o"]),
|
||||
Option("--saturation", Args["saturation", float], alias=["-s"]),
|
||||
Args["image", Image | None],
|
||||
))
|
||||
|
||||
@snaur_display_cmd.handle()
|
||||
async def _(img: PIL_Image, whiteness: float = 0.0, black_level: float = 0.2,
|
||||
opacity: float = 0.8, saturation: float = 0.85):
|
||||
img_processed = await draw_snaur_display(
|
||||
img, whiteness, black_level, opacity, saturation,
|
||||
)
|
||||
img_data = BytesIO()
|
||||
img_processed.save(img_data, "PNG")
|
||||
await snaur_display_cmd.send(await UniMessage().image(raw=img_data).export())
|
||||
|
||||
|
||||
@ -4,10 +4,12 @@ from typing import Any, cast
|
||||
import cv2
|
||||
import numpy as np
|
||||
import PIL.Image
|
||||
import PIL.ImageChops
|
||||
import PIL.ImageEnhance
|
||||
|
||||
from konabot.common.path import ASSETS_PATH
|
||||
|
||||
cao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "caoimg1.png")
|
||||
cao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "caoimg1.png")
|
||||
CAO_QUAD_POINTS = np.float32(cast(Any, [
|
||||
[392, 540],
|
||||
[577, 557],
|
||||
@ -15,6 +17,16 @@ CAO_QUAD_POINTS = np.float32(cast(Any, [
|
||||
[381, 687],
|
||||
]))
|
||||
|
||||
snaur_image_base = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "snaur_1_base.png")
|
||||
snaur_image_top = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "snaur_1_top.png")
|
||||
SNAUR_RATIO = (1 / 2) ** .5
|
||||
SNAUR_QUAD_POINTS = np.float32(cast(Any, [
|
||||
[0, 466 ],
|
||||
[673, 471 ],
|
||||
[640, 1196],
|
||||
[106, 1280],
|
||||
]))
|
||||
|
||||
def _draw_cao_display(image: PIL.Image.Image):
|
||||
src = np.array(image.convert("RGB"))
|
||||
h, w = src.shape[:2]
|
||||
@ -43,3 +55,87 @@ def _draw_cao_display(image: PIL.Image.Image):
|
||||
|
||||
async def draw_cao_display(image: PIL.Image.Image):
|
||||
return await asyncio.to_thread(_draw_cao_display, image)
|
||||
|
||||
|
||||
def _draw_snaur_display(
|
||||
image : PIL.Image.Image,
|
||||
whiteness : float = 0.0 ,
|
||||
black_level: float = 0.2 ,
|
||||
opacity : float = 0.8 ,
|
||||
saturation : float = 0.85 ,
|
||||
):
|
||||
src = np.array(image.convert("RGBA"))
|
||||
_h, _w = src.shape[:2]
|
||||
|
||||
if _w / _h < SNAUR_RATIO:
|
||||
_w_target = _w
|
||||
_h_target = int(_w / SNAUR_RATIO)
|
||||
else:
|
||||
_w_target = int(_h * SNAUR_RATIO)
|
||||
_h_target = _h
|
||||
|
||||
x_center = _w / 2
|
||||
y_center = _h / 2
|
||||
|
||||
x1 = int(x_center - _w_target / 2)
|
||||
x2 = int(x_center + _w_target / 2)
|
||||
y1 = int(y_center - _h_target / 2)
|
||||
y2 = int(y_center + _h_target / 2)
|
||||
|
||||
src = src[y1:y2, x1:x2, :]
|
||||
|
||||
h, w = src.shape[:2]
|
||||
src_points = np.float32(cast(Any, [
|
||||
[0, 0],
|
||||
[w, 0],
|
||||
[w, h],
|
||||
[0, h],
|
||||
]))
|
||||
dst_points = SNAUR_QUAD_POINTS
|
||||
M = cv2.getPerspectiveTransform(cast(Any, src_points), cast(Any, dst_points))
|
||||
output_size = snaur_image_top.size
|
||||
output_w, output_h = output_size
|
||||
warped = cv2.warpPerspective(
|
||||
src,
|
||||
M,
|
||||
(output_w, output_h),
|
||||
flags=cv2.INTER_LINEAR,
|
||||
borderMode=cv2.BORDER_CONSTANT,
|
||||
borderValue=(0, 0, 0)
|
||||
)
|
||||
|
||||
result = PIL.Image.fromarray(warped, 'RGBA')
|
||||
|
||||
r, g, b, a = result.split()
|
||||
a = a.point(lambda p: int(p * opacity))
|
||||
f2 = lambda p: int(
|
||||
((p / 255) ** (2 ** whiteness)) * 255 * (1 - black_level)
|
||||
+ 255 * black_level
|
||||
)
|
||||
r = r.point(f2)
|
||||
g = g.point(f2)
|
||||
b = b.point(f2)
|
||||
result = PIL.Image.merge('RGBA', (r, g, b, a))
|
||||
|
||||
enhancer = PIL.ImageEnhance.Color(result)
|
||||
result = enhancer.enhance(saturation)
|
||||
|
||||
result = PIL.ImageChops.multiply(result, snaur_image_base)
|
||||
|
||||
result = PIL.Image.alpha_composite(snaur_image_base, result)
|
||||
result = PIL.Image.alpha_composite(result, snaur_image_top)
|
||||
return result
|
||||
|
||||
|
||||
async def draw_snaur_display(
|
||||
image : PIL.Image.Image,
|
||||
whiteness : float = 0.0 ,
|
||||
black_level: float = 0.2 ,
|
||||
opacity : float = 0.8 ,
|
||||
saturation : float = 0.85 ,
|
||||
) -> PIL.Image.Image:
|
||||
return await asyncio.to_thread(
|
||||
_draw_snaur_display, image, whiteness, black_level,
|
||||
opacity, saturation,
|
||||
)
|
||||
|
||||
|
||||
168
konabot/plugins/poll/__init__.py
Normal file
168
konabot/plugins/poll/__init__.py
Normal file
@ -0,0 +1,168 @@
|
||||
import json, time
|
||||
|
||||
from nonebot.rule import Rule
|
||||
from nonebot_plugin_alconna import Alconna, Args, Field, MultiVar, on_alconna
|
||||
from nonebot.adapters.onebot.v11 import Event
|
||||
|
||||
from konabot.common.nb.wzq_conflict import no_wzqbot_rule
|
||||
from konabot.common.path import ASSETS_PATH, DATA_PATH
|
||||
|
||||
|
||||
POLL_TEMPLATE_FILE = ASSETS_PATH / "json" / "poll.json"
|
||||
POLL_DATA_FILE = DATA_PATH / "poll.json"
|
||||
|
||||
if not POLL_DATA_FILE.exists():
|
||||
POLL_DATA_FILE.write_bytes(POLL_TEMPLATE_FILE.read_bytes())
|
||||
|
||||
|
||||
poll_list = json.loads(POLL_DATA_FILE.read_text())['poll']
|
||||
|
||||
async def createpoll(title,qqid,options):
|
||||
polllength = len(poll_list)
|
||||
pollid = str(polllength)
|
||||
poll_create = int(time.time())
|
||||
poll_expiry = poll_create + 24*3600
|
||||
polljson = {"title":title,"qq":qqid,"create":poll_create,"expiry":poll_expiry,"options":options,"polldata":{}}
|
||||
poll_list[pollid] = polljson
|
||||
writeback()
|
||||
return pollid
|
||||
|
||||
def getpolldata(pollid_or_title):
|
||||
# 初始化“被指定的投票项目”
|
||||
thepoll = {}
|
||||
polnum = -1
|
||||
# 判断是ID还是标题
|
||||
if str.isdigit(pollid_or_title):
|
||||
if pollid_or_title in poll_list:
|
||||
thepoll = poll_list[pollid_or_title]
|
||||
polnum = pollid_or_title
|
||||
else:
|
||||
return [{},-1]
|
||||
else:
|
||||
for i in poll_list:
|
||||
if poll_list[i]["title"] == pollid_or_title:
|
||||
thepoll = poll_list[i]
|
||||
polnum = i
|
||||
break
|
||||
if polnum == -1:
|
||||
return [{},-1]
|
||||
return [thepoll,polnum]
|
||||
|
||||
def writeback():
|
||||
# file = open(poll_json_path,"w",encoding="utf-8")
|
||||
# json.dump({'poll':poll_list},file,ensure_ascii=False,sort_keys=True)
|
||||
POLL_DATA_FILE.write_text(json.dumps({
|
||||
'poll': poll_list,
|
||||
}, ensure_ascii=False, sort_keys=True))
|
||||
|
||||
async def pollvote(polnum,optionnum,qqnum):
|
||||
optiond = poll_list[polnum]["polldata"]
|
||||
if optionnum not in optiond:
|
||||
poll_list[polnum]["polldata"][optionnum] = []
|
||||
poll_list[polnum]["polldata"][optionnum].append(qqnum)
|
||||
writeback()
|
||||
return
|
||||
|
||||
poll = on_alconna(Alconna(
|
||||
"poll",
|
||||
Args["saying", MultiVar(str, '+'), Field(
|
||||
missing_tips=lambda: "参数错误。用法:发起投票 <投票标题> <选项1> <选项2> ..."
|
||||
)],
|
||||
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"发起投票","createpoll"}, rule=no_wzqbot_rule)
|
||||
@poll.handle()
|
||||
async def _(saying: list, event: Event):
|
||||
if (len(saying) < 3):
|
||||
await poll.send("请提供至少两个投票选项!")
|
||||
elif (len(saying) < 17):
|
||||
title = saying[0]
|
||||
saying.remove(title)
|
||||
options = {}
|
||||
for i in saying:
|
||||
options[saying.index(i)] = i
|
||||
qqid = event.get_user_id()
|
||||
result = await createpoll(title,qqid,options)
|
||||
await poll.send("已创建投票。回复 查看投票 "+str(result)+" 查看该投票。")
|
||||
else:
|
||||
await poll.send("投票选项太多了!请减少到15个选项以内。")
|
||||
|
||||
viewpoll = on_alconna(Alconna(
|
||||
"viewpoll",
|
||||
Args["saying", MultiVar(str, '+'), Field(
|
||||
missing_tips=lambda: "请指定投票ID或标题!。用法:查看投票 <投票ID或标题>"
|
||||
)],
|
||||
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"查看投票"}, rule=no_wzqbot_rule)
|
||||
@viewpoll.handle()
|
||||
async def _(saying: list):
|
||||
# 参数,投票ID或者标题
|
||||
# pollid_or_title = params[0]
|
||||
polldata = getpolldata(saying[0])
|
||||
# 被指定的投票项目
|
||||
thepoll = polldata[0]
|
||||
polnum = polldata[1]
|
||||
if polnum == -1:
|
||||
await viewpoll.send("该投票不存在!")
|
||||
else:
|
||||
# 检查投票是否已结束
|
||||
pollended = 0
|
||||
if time.time() > thepoll["expiry"]:
|
||||
pollended = 1
|
||||
# 回复内容
|
||||
reply = "投票:"+thepoll["title"]+" [ID: "+str(polnum)+"]"
|
||||
# 如果投票已结束
|
||||
if pollended:
|
||||
for i in thepoll["options"]:
|
||||
reply += "\n"
|
||||
# 检查该选项是否有人投票
|
||||
if i in thepoll["polldata"]:
|
||||
reply += "["+str(len(thepoll["polldata"][i]))+" 票]"
|
||||
else:
|
||||
reply += "[0 票]"
|
||||
reply += " "+thepoll["options"][i]
|
||||
reply += "\n\n此投票已结束。"
|
||||
else:
|
||||
for i in thepoll["options"]:
|
||||
reply += "\n"
|
||||
reply += "- "+thepoll["options"][i]
|
||||
# reply += "\n\n小提示:向bot私聊发送 /viewpoll "+str(polnum)+" 可查看已投票数哦!"
|
||||
reply += "\n\n发送 投票 "+str(polnum)+" <选项文本> 即可参与投票!"
|
||||
await viewpoll.send(reply)
|
||||
|
||||
vote = on_alconna(Alconna(
|
||||
"vote",
|
||||
Args["saying", MultiVar(str, '+'), Field(
|
||||
missing_tips=lambda: "参数错误。用法:投票 <投票ID/标题> <选项文本>"
|
||||
)],
|
||||
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"投票","参与投票"}, rule=no_wzqbot_rule)
|
||||
@vote.handle()
|
||||
async def _(saying: list, event: Event):
|
||||
if (len(saying) < 2):
|
||||
await vote.send("请指定投给哪一项!")
|
||||
else:
|
||||
polldata = getpolldata(saying[0])
|
||||
# 被指定的投票项目
|
||||
thepoll = polldata[0]
|
||||
polnum = polldata[1]
|
||||
if polnum == -1:
|
||||
await viewpoll.finish("没有找到这个投票!")
|
||||
# thepolldata = thepoll["polldata"]
|
||||
# 查找对应的投票项
|
||||
optionnum = -1
|
||||
for i in thepoll["options"]:
|
||||
if saying[1] == thepoll["options"][i]:
|
||||
optionnum = i
|
||||
break
|
||||
if optionnum == -1:
|
||||
reply = "此投票里面没有这一项!可用的选项有:"
|
||||
for i in thepoll["options"]:
|
||||
reply += "\n"
|
||||
reply += "- "+thepoll["options"][i]
|
||||
await viewpoll.send(reply)
|
||||
# 检查是否符合投票条件(该qq号是否已参与过投票、投票是否过期)
|
||||
elif time.time() > thepoll["expiry"]:
|
||||
await viewpoll.send("此投票已经结束!请发送 查看投票 "+polnum+" 查看结果。")
|
||||
elif str(event.get_user_id()) in str(thepoll["polldata"]):
|
||||
await viewpoll.send("你已参与过此投票!请在投票结束后发送 查看投票 "+polnum+" 查看结果。")
|
||||
# 写入项目
|
||||
else:
|
||||
await pollvote(polnum,optionnum,event.get_user_id())
|
||||
await viewpoll.send("投票成功!你投给了 "+saying[1])
|
||||
@ -309,7 +309,7 @@ async def generate_dice_image(number: str) -> BytesIO:
|
||||
if(len(text) > 50):
|
||||
output = BytesIO()
|
||||
push_image = Image.open(ASSETS_PATH / "img" / "dice" / "stick.png")
|
||||
push_image.save(output,format='PNG')
|
||||
push_image.save(output,format='GIF')
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import asyncio as asynkio
|
||||
import datetime
|
||||
import functools
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import signal
|
||||
import nonebot
|
||||
import ptimeparse
|
||||
from loguru import logger
|
||||
@ -24,7 +26,9 @@ evt = on_message()
|
||||
|
||||
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
|
||||
DATA_FILE_PATH = Path(__file__).parent.parent.parent.parent / "data" / "notify.json"
|
||||
DATA_FILE_LOCK = asyncio.Lock()
|
||||
DATA_FILE_LOCK = asynkio.Lock()
|
||||
|
||||
ASYNK_TASKS: set[asynkio.Task[Any]] = set()
|
||||
|
||||
|
||||
class Notify(BaseModel):
|
||||
@ -107,11 +111,20 @@ async def notify_now(notify: Notify):
|
||||
return True
|
||||
|
||||
|
||||
async def create_notify_task(notify: Notify, fail2remove: bool = True):
|
||||
def create_notify_task(notify: Notify, fail2remove: bool = True):
|
||||
async def mission():
|
||||
begin_time = datetime.datetime.now()
|
||||
if begin_time < notify.notify_time:
|
||||
await asyncio.sleep((notify.notify_time - begin_time).total_seconds())
|
||||
try:
|
||||
await asynkio.sleep((notify.notify_time - begin_time).total_seconds())
|
||||
except asynkio.CancelledError:
|
||||
logger.debug("代办提醒被信号中止,任务退出")
|
||||
return
|
||||
else:
|
||||
logger.warning(
|
||||
f"期望在 {notify.notify_time} 在平台 {notify.platform} {notify.target_env}"
|
||||
f" {notify.target} 的代办通知 {notify.notify_msg} 已经超时,将会直接通知!"
|
||||
)
|
||||
res = await notify_now(notify)
|
||||
if fail2remove or res:
|
||||
await DATA_FILE_LOCK.acquire()
|
||||
@ -123,7 +136,7 @@ async def create_notify_task(notify: Notify, fail2remove: bool = True):
|
||||
DATA_FILE_LOCK.release()
|
||||
else:
|
||||
pass
|
||||
return asyncio.create_task(mission())
|
||||
return asynkio.create_task(mission())
|
||||
|
||||
|
||||
@evt.handle()
|
||||
@ -143,6 +156,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
||||
# target_time = get_target_time(notify_time)
|
||||
try:
|
||||
target_time = ptimeparse.parse(notify_time)
|
||||
logger.info(f"从 {notify_time} 解析出了时间:{target_time}")
|
||||
except Exception:
|
||||
logger.info(f"无法从 {notify_time} 中解析出时间")
|
||||
return
|
||||
@ -154,7 +168,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
||||
|
||||
await DATA_FILE_LOCK.acquire()
|
||||
cfg = load_notify_config()
|
||||
|
||||
|
||||
if isinstance(mEvt, ConsoleMessageEvent):
|
||||
platform = "console"
|
||||
target = mEvt.get_user_id()
|
||||
@ -181,7 +195,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
||||
notify_time=target_time,
|
||||
notify_msg=notify_text,
|
||||
)
|
||||
await create_notify_task(notify)
|
||||
create_notify_task(notify)
|
||||
|
||||
cfg.notifies.append(notify)
|
||||
save_notify_config(cfg)
|
||||
@ -189,6 +203,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
||||
|
||||
await evt.send(await UniMessage().at(mEvt.get_user_id()).text(
|
||||
f" 了解啦!将会在 {notify.notify_time} 提醒你哦~").export())
|
||||
logger.info(f"创建了一条于 {notify.notify_time} 的代办提醒")
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
@ -205,15 +220,41 @@ async def _():
|
||||
|
||||
NOTIFIED_FLAG["task_added"] = True
|
||||
|
||||
await asyncio.sleep(10)
|
||||
DELTA = 2
|
||||
logger.info(f"第一次探测到 Bot 连接,等待 {DELTA} 秒后开始通知")
|
||||
await asynkio.sleep(DELTA)
|
||||
|
||||
await DATA_FILE_LOCK.acquire()
|
||||
tasks = []
|
||||
|
||||
# tasks: set[asynkio.Task[Any]] = set()
|
||||
cfg = load_notify_config()
|
||||
if cfg.version == 1:
|
||||
logger.info("将配置文件的版本升级为 2")
|
||||
cfg.version = 2
|
||||
else:
|
||||
for notify in cfg.notifies:
|
||||
tasks.append(create_notify_task(notify, fail2remove=False))
|
||||
counter = 0
|
||||
for notify in [*cfg.notifies]:
|
||||
task = create_notify_task(notify, fail2remove=False)
|
||||
ASYNK_TASKS.add(task)
|
||||
task.add_done_callback(lambda self: ASYNK_TASKS.remove(self))
|
||||
counter += 1
|
||||
logger.info(f"成功创建了 {counter} 条代办事项")
|
||||
save_notify_config(cfg)
|
||||
DATA_FILE_LOCK.release()
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
loop = asynkio.get_running_loop()
|
||||
|
||||
# 解决 asynk task 没有被 cancel 的问题
|
||||
async def shutdown(sig: signal.Signals):
|
||||
logger.info(f"收到 {sig.name} 指令,正在关闭所有的东西")
|
||||
for task in ASYNK_TASKS:
|
||||
task.cancel()
|
||||
await asynkio.gather(*ASYNK_TASKS, return_exceptions=True)
|
||||
logger.info("所有的代办提醒 Task 都已经退出了")
|
||||
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
loop.add_signal_handler(sig, functools.partial(
|
||||
asynkio.create_task, shutdown(sig)
|
||||
))
|
||||
|
||||
await asynkio.gather(*ASYNK_TASKS)
|
||||
|
||||
565
poetry.lock
generated
565
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,8 @@ dependencies = [
|
||||
"returns (>=0.26.0,<0.27.0)",
|
||||
"ptimeparse (>=0.1.1,<0.2.0)",
|
||||
"skia-python (>=138.0,<139.0)",
|
||||
"nonebot-plugin-analysis-bilibili (>=2.8.1,<3.0.0)",
|
||||
"qrcode (>=8.2,<9.0)",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
@ -35,5 +37,10 @@ name = "pt-gitea-pypi"
|
||||
url = "https://gitea.service.jazzwhom.top/api/packages/Passthem/pypi/simple/"
|
||||
priority = "supplemental"
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "mirrors"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
|
||||
priority = "primary"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
ptimeparse = {source = "pt-gitea-pypi"}
|
||||
|
||||
Reference in New Issue
Block a user