语法糖
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"python.REPL.enableREPLSmartSend": false
|
"python.REPL.enableREPLSmartSend": false,
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:poetry",
|
||||||
|
"python-envs.defaultPackageManager": "ms-python.python:poetry"
|
||||||
}
|
}
|
||||||
210
konabot/plugins/syntactic_sugar/__init__.py
Normal file
210
konabot/plugins/syntactic_sugar/__init__.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import copy
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
from nonebot import on_command
|
||||||
|
from nonebot.adapters import Bot, Event, Message
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.message import handle_event
|
||||||
|
from nonebot.params import CommandArg
|
||||||
|
|
||||||
|
from konabot.common.database import DatabaseManager
|
||||||
|
from konabot.common.longtask import DepLongTaskTarget
|
||||||
|
|
||||||
|
ROOT_PATH = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
cmd = on_command(cmd="语法糖", aliases={"糖", "sugar"}, block=True)
|
||||||
|
|
||||||
|
db_manager = DatabaseManager()
|
||||||
|
driver = nonebot.get_driver()
|
||||||
|
|
||||||
|
|
||||||
|
@driver.on_startup
|
||||||
|
async def register_startup_hook():
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
|
||||||
|
@driver.on_shutdown
|
||||||
|
async def register_shutdown_hook():
|
||||||
|
await db_manager.close_all_connections()
|
||||||
|
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
await db_manager.execute_by_sql_file(ROOT_PATH / "sql" / "create_table.sql")
|
||||||
|
|
||||||
|
table_info = await db_manager.query("PRAGMA table_info(syntactic_sugar)")
|
||||||
|
columns = {str(row.get("name")) for row in table_info}
|
||||||
|
if "channel_id" not in columns:
|
||||||
|
await db_manager.execute(
|
||||||
|
"ALTER TABLE syntactic_sugar ADD COLUMN channel_id VARCHAR(255) NOT NULL DEFAULT ''"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db_manager.execute("DROP INDEX IF EXISTS idx_syntactic_sugar_name_belong_to")
|
||||||
|
await db_manager.execute(
|
||||||
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_syntactic_sugar_name_channel_target "
|
||||||
|
"ON syntactic_sugar(name, channel_id, belong_to)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_reply_plain_text(evt: Event) -> str:
|
||||||
|
reply = getattr(evt, "reply", None)
|
||||||
|
if reply is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
reply_message = getattr(reply, "message", None)
|
||||||
|
if reply_message is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
extract_plain_text = getattr(reply_message, "extract_plain_text", None)
|
||||||
|
if callable(extract_plain_text):
|
||||||
|
return extract_plain_text().strip()
|
||||||
|
return str(reply_message).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _split_variables(tokens: list[str]) -> tuple[list[str], dict[str, str]]:
|
||||||
|
positional: list[str] = []
|
||||||
|
named: dict[str, str] = {}
|
||||||
|
|
||||||
|
for token in tokens:
|
||||||
|
if "=" in token:
|
||||||
|
key, value = token.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
if key:
|
||||||
|
named[key] = value
|
||||||
|
continue
|
||||||
|
positional.append(token)
|
||||||
|
|
||||||
|
return positional, named
|
||||||
|
|
||||||
|
|
||||||
|
def _render_template(content: str, positional: list[str], named: dict[str, str]) -> str:
|
||||||
|
def replace(match: re.Match[str]) -> str:
|
||||||
|
key = match.group(1).strip()
|
||||||
|
if key.isdigit():
|
||||||
|
idx = int(key) - 1
|
||||||
|
if 0 <= idx < len(positional):
|
||||||
|
return positional[idx]
|
||||||
|
return match.group(0)
|
||||||
|
return named.get(key, match.group(0))
|
||||||
|
|
||||||
|
return re.sub(r"\{([^{}]+)\}", replace, content)
|
||||||
|
|
||||||
|
|
||||||
|
async def _store_sugar(name: str, content: str, belong_to: str, channel_id: str):
|
||||||
|
await db_manager.execute_by_sql_file(
|
||||||
|
ROOT_PATH / "sql" / "insert_sugar.sql",
|
||||||
|
(name, content, belong_to, channel_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_sugar(name: str, belong_to: str, channel_id: str):
|
||||||
|
await db_manager.execute(
|
||||||
|
"DELETE FROM syntactic_sugar WHERE name = ? AND belong_to = ? AND channel_id = ?",
|
||||||
|
(name, belong_to, channel_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_sugar(name: str, belong_to: str, channel_id: str) -> str | None:
|
||||||
|
rows = await db_manager.query(
|
||||||
|
(
|
||||||
|
"SELECT content FROM syntactic_sugar "
|
||||||
|
"WHERE name = ? AND channel_id = ? "
|
||||||
|
"ORDER BY CASE WHEN belong_to = ? THEN 0 ELSE 1 END, id ASC "
|
||||||
|
"LIMIT 1"
|
||||||
|
),
|
||||||
|
(name, channel_id, belong_to),
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
return rows[0].get("content")
|
||||||
|
|
||||||
|
|
||||||
|
async def _reinject_command(bot: Bot, evt: Event, command_text: str) -> bool:
|
||||||
|
depth = int(getattr(evt, "_syntactic_sugar_depth", 0))
|
||||||
|
if depth >= 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cloned_evt = copy.deepcopy(evt)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("语法糖克隆事件失败")
|
||||||
|
return False
|
||||||
|
|
||||||
|
message = getattr(cloned_evt, "message", None)
|
||||||
|
if message is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg_obj = type(message)(command_text)
|
||||||
|
except Exception:
|
||||||
|
msg_obj = command_text
|
||||||
|
|
||||||
|
setattr(cloned_evt, "message", msg_obj)
|
||||||
|
if hasattr(cloned_evt, "original_message"):
|
||||||
|
setattr(cloned_evt, "original_message", msg_obj)
|
||||||
|
if hasattr(cloned_evt, "raw_message"):
|
||||||
|
setattr(cloned_evt, "raw_message", command_text)
|
||||||
|
|
||||||
|
setattr(cloned_evt, "_syntactic_sugar_depth", depth + 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await handle_event(bot, cloned_evt)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("语法糖回注事件失败")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def _(bot: Bot, evt: Event, target: DepLongTaskTarget, args: Message = CommandArg()):
|
||||||
|
raw = args.extract_plain_text().strip()
|
||||||
|
if not raw:
|
||||||
|
return
|
||||||
|
|
||||||
|
tokens = raw.split()
|
||||||
|
action = tokens[0]
|
||||||
|
target_id = target.target_id
|
||||||
|
channel_id = target.channel_id
|
||||||
|
|
||||||
|
if action == "存入":
|
||||||
|
if len(tokens) < 2:
|
||||||
|
await cmd.finish("请提供要存入的名称")
|
||||||
|
name = tokens[1].strip()
|
||||||
|
content = " ".join(tokens[2:]).strip()
|
||||||
|
if not content:
|
||||||
|
content = _extract_reply_plain_text(evt)
|
||||||
|
if not content:
|
||||||
|
await cmd.finish("请提供要存入的内容")
|
||||||
|
|
||||||
|
await _store_sugar(name, content, target_id, channel_id)
|
||||||
|
await cmd.finish(f"糖已存入:「{name}」!")
|
||||||
|
|
||||||
|
if action == "删除":
|
||||||
|
if len(tokens) < 2:
|
||||||
|
await cmd.finish("请提供要删除的名称")
|
||||||
|
name = tokens[1].strip()
|
||||||
|
await _delete_sugar(name, target_id, channel_id)
|
||||||
|
await cmd.finish(f"已删除糖:「{name}」!")
|
||||||
|
|
||||||
|
if action == "查看":
|
||||||
|
if len(tokens) < 2:
|
||||||
|
await cmd.finish("请提供要查看的名称")
|
||||||
|
name = tokens[1].strip()
|
||||||
|
content = await _find_sugar(name, target_id, channel_id)
|
||||||
|
if content is None:
|
||||||
|
await cmd.finish(f"没有糖:「{name}」")
|
||||||
|
await cmd.finish(f"糖的内容:「{content}」")
|
||||||
|
|
||||||
|
|
||||||
|
name = action
|
||||||
|
content = await _find_sugar(name, target_id, channel_id)
|
||||||
|
if content is None:
|
||||||
|
await cmd.finish(f"没有糖:「{name}」")
|
||||||
|
|
||||||
|
positional, named = _split_variables(tokens[1:])
|
||||||
|
rendered = _render_template(content, positional, named)
|
||||||
|
|
||||||
|
ok = await _reinject_command(bot, evt, rendered)
|
||||||
|
if not ok:
|
||||||
|
await cmd.finish(f"糖的展开结果:「{rendered}」")
|
||||||
12
konabot/plugins/syntactic_sugar/sql/create_table.sql
Normal file
12
konabot/plugins/syntactic_sugar/sql/create_table.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- 创建语法糖表
|
||||||
|
CREATE TABLE IF NOT EXISTS syntactic_sugar (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
belong_to VARCHAR(255) NOT NULL,
|
||||||
|
channel_id VARCHAR(255) NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_syntactic_sugar_name_channel_target
|
||||||
|
ON syntactic_sugar(name, channel_id, belong_to);
|
||||||
5
konabot/plugins/syntactic_sugar/sql/insert_sugar.sql
Normal file
5
konabot/plugins/syntactic_sugar/sql/insert_sugar.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- 插入语法糖,如果同一用户下名称已存在则更新内容
|
||||||
|
INSERT INTO syntactic_sugar (name, content, belong_to, channel_id)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(name, channel_id, belong_to) DO UPDATE SET
|
||||||
|
content = excluded.content;
|
||||||
Reference in New Issue
Block a user