Compare commits

...

9 Commits

Author SHA1 Message Date
9265c250b3 补充一些权限系统有关的注释
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-20 19:34:12 +08:00
4dd9320678 取消罗文的反应机制
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-20 19:11:46 +08:00
db96202d5d 添加小睦想
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-19 00:11:32 +08:00
881b08c41f 此方晚安文档更新
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-13 15:33:40 +08:00
c11d29e136 Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot
Some checks failed
continuous-integration/drone/push Build is failing
2026-05-13 15:25:25 +08:00
1fa74b61d6 更新各种依赖 2026-05-13 15:25:11 +08:00
f0601acbe9 oyasumi
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-13 15:06:49 +08:00
39c7c043ca Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-29 18:52:01 +08:00
39accb16e0 桂花说 2026-04-29 18:51:37 +08:00
11 changed files with 1013 additions and 785 deletions

BIN
assets/img/meme/xiaomu.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -29,10 +29,21 @@ async def _to_entity_chain(el: _EntityLike):
class PermManager: class PermManager:
"""
权限管理模块
"""
def __init__(self, db: DatabaseManager) -> None: def __init__(self, db: DatabaseManager) -> None:
self.db = db self.db = db
async def get_permission_info(self, entities: _EntityLike, key: str): async def get_permission_info(
self, entities: _EntityLike, key: str
) -> tuple[PermEntity, str, bool] | None:
"""
获得一个权限实体或权限实体串对一个 key 的权限信息。若未入库(默认值)则
代表没有该权限相关的记录
"""
entities = await _to_entity_chain(entities) entities = await _to_entity_chain(entities)
key = key.removesuffix("*").removesuffix(".") key = key.removesuffix("*").removesuffix(".")
key_split = key.split(".") key_split = key.split(".")
@ -52,17 +63,29 @@ class PermManager:
return None return None
async def check_has_permission(self, entities: _EntityLike, key: str) -> bool: async def check_has_permission(self, entities: _EntityLike, key: str) -> bool:
"""
检查一个权限实体或者权限实体串是否有权限
"""
res = await self.get_permission_info(entities, key) res = await self.get_permission_info(entities, key)
if res is None: if res is None:
return False return False
return res[2] return res[2]
async def update_permission(self, entity: PermEntity, key: str, perm: bool | None): async def update_permission(self, entity: PermEntity, key: str, perm: bool | None):
"""
更新一个具体的权限实体的一则权限
"""
async with self.db.get_conn() as conn: async with self.db.get_conn() as conn:
repo = PermRepo(conn) repo = PermRepo(conn)
await repo.update_perm_info(entity, key, perm) await repo.update_perm_info(entity, key, perm)
async def list_permission(self, entities: _EntityLike, query: PagerQuery): async def list_permission(self, entities: _EntityLike, query: PagerQuery):
"""
列出一个权限实体或权限实体串拥有的所有权限记录
"""
entities = await _to_entity_chain(entities) entities = await _to_entity_chain(entities)
async with self.db.get_conn() as conn: async with self.db.get_conn() as conn:
repo = PermRepo(conn) repo = PermRepo(conn)
@ -113,6 +136,22 @@ def register_default_allow_permission(key: str):
def require_permission(perm: str) -> Rule: # pragma: no cover def require_permission(perm: str) -> Rule: # pragma: no cover
"""
`require_permission` 是一个 Nonebot 规则,可以用来要求一个 Nonebot 的指令需
要拥有一定的权限。
```python
from konabot.common.permsys import require_permission
from nonebot import on_command
cmd = on_command("kz", rule=require_permission("kagami.kz"))
@cmd.handle()
async def _():
await cmd.finish("你抓到了普通pt")
```
"""
async def check_permission(event: Event, pm: DepPermManager) -> bool: async def check_permission(event: Event, pm: DepPermManager) -> bool:
return await pm.check_has_permission(event, perm) return await pm.check_has_permission(event, perm)

View File

@ -22,6 +22,11 @@ class PermEntity:
def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]: def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]:
"""
获得一个权限实体的权限串。实际上返回三个权限,从小到大分别是用户、平台全体和
系统全局的权限实体。
"""
return [ return [
PermEntity("sys", "global", "global"), PermEntity("sys", "global", "global"),
PermEntity(entity.platform, "global", "global"), PermEntity(entity.platform, "global", "global"),
@ -30,6 +35,10 @@ def get_entity_chain_of_entity(entity: PermEntity) -> list[PermEntity]:
async def get_entity_chain(event: Event) -> list[PermEntity]: # pragma: no cover async def get_entity_chain(event: Event) -> list[PermEntity]: # pragma: no cover
"""
获得一个 Nonebot Event 的权限实体串。
"""
entities = [PermEntity("sys", "global", "global")] entities = [PermEntity("sys", "global", "global")]
if isinstance(event, OB11Event): if isinstance(event, OB11Event):

View File

@ -1,3 +0,0 @@
# 关于罗文和洛温
AdoreLowen 希望和洛温阿特金森区分,所以最好就不要叫他洛温了!此方 BOT 会在一些群提醒叫错了的人。

View File

@ -0,0 +1,8 @@
# 指令介绍
**此方晚安** - 让此方 BOT 禁言你一段时间
## 指令格式
- `@此方BOT 此方晚安`: 禁言几个小时
- `@此方BOT 此方午安`: 禁言几十分钟

View File

@ -0,0 +1,44 @@
import re
from typing import Any
from nonebot import on_message
from nonebot.adapters import Event
from nonebot_plugin_alconna import UniMessage, UniMsg
from playwright.async_api import Page
from konabot.common.nb import match_keyword
from konabot.common.web_render import WebRenderer, konaweb
async def render_image(message: str) -> UniMessage[Any]:
"""
渲染文本为图片
"""
async def page_function(page: Page):
await page.wait_for_function("typeof setContent === 'function'")
await page.evaluate(
"([ message ]) => { return setContent(message); }",
[ message ],
)
img_data = await WebRenderer.render(
url=konaweb("guihuasay"),
target="#main",
other_function=page_function,
)
return UniMessage.image(raw=img_data)
cmd = on_message(
rule=match_keyword.match_keyword(
re.compile(r"^(桂花[说想])\s.+", re.I),
),
)
@cmd.handle()
async def _(event: Event, msg: UniMsg):
text = msg.extract_plain_text().lstrip()
_, content = text.split(maxsplit=1)
msg = await render_image(content)
await msg.send(event)

View File

@ -1,6 +1,8 @@
from io import BytesIO from io import BytesIO
from typing import Iterable, cast from typing import Iterable, cast
import PIL.Image
from loguru import logger from loguru import logger
from nonebot import on_message from nonebot import on_message
from nonebot_plugin_alconna import ( from nonebot_plugin_alconna import (
@ -18,7 +20,7 @@ from nonebot_plugin_alconna import (
from playwright.async_api import ConsoleMessage, Page from playwright.async_api import ConsoleMessage, Page
from konabot.common.nb.match_keyword import match_keyword from konabot.common.nb.match_keyword import match_keyword
from konabot.common.nb.extract_image import DepPILImage from konabot.common.nb.extract_image import DepImageBytesOrNone, DepPILImage
from konabot.common.web_render import konaweb from konabot.common.web_render import konaweb
from konabot.common.web_render.core import WebRenderer from konabot.common.web_render.core import WebRenderer
from konabot.common.web_render.host_images import host_tempdir from konabot.common.web_render.host_images import host_tempdir
@ -35,6 +37,7 @@ from konabot.plugins.memepack.drawing.saying import (
draw_pt, draw_pt,
draw_suan, draw_suan,
draw_vr, draw_vr,
draw_xm
) )
from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark
@ -361,3 +364,33 @@ async def _(saying: list[str]):
await vrsay.send(await UniMessage().image(raw=img_bytes).export()) await vrsay.send(await UniMessage().image(raw=img_bytes).export())
xmsay = on_alconna(
Alconna(
"小睦说",
Args[
"saying",
MultiVar(str, "*"),
Field(missing_tips=lambda: "你没有写小睦说了什么"),
],
Args["image?", Image | None],
),
use_cmd_start=True,
use_cmd_sep=False,
skip_for_unmatch=False,
aliases={"小睦想"},
)
@xmsay.handle()
async def _(saying: list[str], image: DepImageBytesOrNone):
if image is not None:
img = PIL.Image.open(BytesIO(image))
else:
img = None
img = await draw_xm("\n".join(saying), img)
img_bytes = BytesIO()
img.save(img_bytes, format="PNG")
await xmsay.send(await UniMessage().image(raw=img_bytes).export())

View File

@ -7,23 +7,41 @@ import PIL.Image
from konabot.common.path import ASSETS_PATH from konabot.common.path import ASSETS_PATH
from konabot.common.utils.to_async import make_async from konabot.common.utils.to_async import make_async
from .base.fonts import HARMONYOS_SANS_SC_BLACK, HARMONYOS_SANS_SC_REGULAR, LXGWWENKAI_REGULAR from .base.fonts import (
HARMONYOS_SANS_SC_BLACK,
HARMONYOS_SANS_SC_REGULAR,
LXGWWENKAI_REGULAR,
)
geimao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "geimao.jpg").convert("RGBA") geimao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "geimao.jpg").convert(
"RGBA"
)
pt_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "ptsay.png").convert("RGBA") pt_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "ptsay.png").convert("RGBA")
mnk_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "mnksay.jpg").convert("RGBA") mnk_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "mnksay.jpg").convert("RGBA")
dasuan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "dss.png").convert("RGBA") dasuan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "dss.png").convert("RGBA")
suan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "suanleba.png").convert("RGBA") suan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "suanleba.png").convert(
cute_ten_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "tententen.png").convert("RGBA") "RGBA"
)
cute_ten_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "tententen.png").convert(
"RGBA"
)
kio_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "kiosay.jpg").convert("RGBA") kio_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "kiosay.jpg").convert("RGBA")
vr_image = PIL.Image.open(ASSETS_PATH / 'img' / 'meme' / 'vr.jpg').convert("RGBA") vr_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "vr.jpg").convert("RGBA")
xm_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "xiaomu.png").convert("RGBA")
def _draw_geimao(saying: str): def _draw_geimao(saying: str):
img = geimao_image.copy() img = geimao_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 960, 50, 0.5, 0, 1920, 240, HARMONYOS_SANS_SC_BLACK, saying,
960,
50,
0.5,
0,
1920,
240,
HARMONYOS_SANS_SC_BLACK,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
0.8, 0.8,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -42,7 +60,14 @@ def _draw_pt(saying: str):
img = pt_image.copy() img = pt_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 259, 278, 0.5, 0.5, 360, 48, HARMONYOS_SANS_SC_REGULAR, saying,
259,
278,
0.5,
0.5,
360,
48,
HARMONYOS_SANS_SC_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -59,7 +84,14 @@ def _draw_mnk(saying: str):
img = mnk_image.copy() img = mnk_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 540, 25, 0.5, 0, 1080, 120, HARMONYOS_SANS_SC_BLACK, saying,
540,
25,
0.5,
0,
1080,
120,
HARMONYOS_SANS_SC_BLACK,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
0.8, 0.8,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -81,7 +113,14 @@ def _draw_suan(saying: str, dasuan: bool = False):
img = suan_image.copy() img = suan_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 1020, 290, 0.5, 0.5, 400, 48, LXGWWENKAI_REGULAR, saying,
1020,
290,
0.5,
0.5,
400,
48,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -98,7 +137,14 @@ def _draw_cute_ten(saying: str):
img = cute_ten_image.copy() img = cute_ten_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 390, 479, 0.5, 0.5, 760, 96, LXGWWENKAI_REGULAR, saying,
390,
479,
0.5,
0.5,
760,
96,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -116,7 +162,14 @@ def draw_kiosay(saying: str):
img = kio_image.copy() img = kio_image.copy()
with imagetext_py.Writer(img) as iw: with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, 450, 540, 0.5, 0.5, 900, 96, LXGWWENKAI_REGULAR, saying,
450,
540,
0.5,
0.5,
900,
96,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -131,12 +184,19 @@ def draw_vr(saying: str):
w, h = img.size w, h = img.size
hw = 300 hw = 300
img2 = PIL.Image.new("RGBA", (w, h + hw), 'white') img2 = PIL.Image.new("RGBA", (w, h + hw), "white")
img2.paste(img, (0, hw)) img2.paste(img, (0, hw))
with imagetext_py.Writer(img2) as iw: with imagetext_py.Writer(img2) as iw:
iw.draw_text_wrapped( iw.draw_text_wrapped(
saying, w // 2, hw // 2 + 15, 0.5, 0.5, w, 64, LXGWWENKAI_REGULAR, saying,
w // 2,
hw // 2 + 15,
0.5,
0.5,
w,
64,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")), imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0, 1.0,
imagetext_py.TextAlign.Center, imagetext_py.TextAlign.Center,
@ -145,3 +205,36 @@ def draw_vr(saying: str):
return img2 return img2
@make_async
def draw_xm(saying: str, image: PIL.Image.Image | None = None):
img_base = PIL.Image.new("RGBA", xm_image.size, (255, 255, 255, 255))
with imagetext_py.Writer(img_base) as iw:
iw.draw_text_wrapped(
saying,
442,
200,
0.5,
0.5,
884,
64,
LXGWWENKAI_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0,
imagetext_py.TextAlign.Center,
draw_emojis=True,
)
if image is not None:
image_r = image.copy().convert("RGBA")
width, height = image_r.size
base_width = img_base.size[0]
height = int(height / width * base_width)
image_r = image_r.resize((base_width, height))
# try to align center
y = 215 - image_r.height // 2
img_base.paste(image_r, (0, y), mask=image_r)
img_base.paste(xm_image, (0, 0), mask=xm_image)
return img_base

View File

@ -1,44 +0,0 @@
import nonebot
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
from nonebot_plugin_alconna import UniMsg, UniMessage
from pydantic import BaseModel
class NoLuowenConfig(BaseModel):
plugin_noluowen_qqid: int = -1
plugin_noluowen_enable_group: list[int] = []
config = nonebot.get_plugin_config(NoLuowenConfig)
async def is_luowen_mentioned(evt: GroupMessageEvent, msg: UniMsg) -> bool:
if config.plugin_noluowen_qqid <= 0:
return False
if evt.user_id == config.plugin_noluowen_qqid:
return False
if evt.group_id not in config.plugin_noluowen_enable_group:
return False
txt = msg.extract_plain_text()
if "洛温" not in txt:
return False
if "罗文" in txt:
return False
if "阿特金森" in txt:
return False
return True
evt_luowen_mentioned = nonebot.on_message(rule=is_luowen_mentioned)
@evt_luowen_mentioned.handle()
async def _(evt: GroupMessageEvent, bot: Bot):
msg = (
UniMessage()
.reply(str(evt.message_id))
.at(str(config.plugin_noluowen_qqid))
.text(" 好像有人念错了你的 ID")
)
await evt_luowen_mentioned.send(await msg.export(bot=bot))

View File

@ -0,0 +1,64 @@
import random
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot.rule import to_me
from nonebot_plugin_alconna import UniMessage
from konabot.common.permsys import require_permission
async def make_sleep(event: GroupMessageEvent, bot: Bot, duration: int):
"""
让人睡着
"""
await bot.set_group_ban(
group_id=event.group_id,
user_id=event.user_id,
duration=duration,
)
seconds = duration % 60
minutes = (duration // 60) % 60
hours = duration // 3600
t1 = f"{hours} 小时 {minutes} 分钟 {seconds}"
message = f" 好好睡吧!奖励你 {t1}的睡眠💤"
message = UniMessage.at(str(event.user_id)).text(message)
await message.send(target=event, bot=bot)
cmd_sleep_night = on_command(
"此方晚安",
rule=require_permission("oyasumi") & to_me(),
aliases={"晚安"},
)
@cmd_sleep_night.handle()
async def oyasumi(event: GroupMessageEvent, bot: Bot):
"""
限定只能用 GroupMessageEvent因为它只能在 QQ 群中使用
"""
# 考虑到有人是熬夜很久,所以这里就给一个 3 到 5 小时睡眠的随机数。这个时间内
# 要睡不着我觉得是个小概率事件了!
duration = random.randint(3 * 3600, 5 * 3600)
await make_sleep(event, bot, duration)
await cmd_sleep_night.finish()
cmd_sleep_noon = on_command(
"此方午安",
rule=require_permission("oyasumi") & to_me(),
aliases={"午安"},
)
@cmd_sleep_noon.handle()
async def sleep_noon(event: GroupMessageEvent, bot: Bot):
duration = random.randint(60 * 15, 60 * 30)
await make_sleep(event, bot, duration)
await cmd_sleep_night.finish()

1431
poetry.lock generated

File diff suppressed because it is too large Load Diff