Compare commits
9 Commits
4f0a9af2dc
...
v0.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
| fc5b11c5e8 | |||
| 0ec66988fa | |||
| e5c3081c22 | |||
| 14b356120a | |||
| a208302cb9 | |||
| 01ffa451bb | |||
| 2b6c2e84bd | |||
| 4c8625ae02 | |||
| c5f820a1f9 |
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": "我~是~谁~?"}}}
|
||||
360393
assets/lexicon/common.txt
Normal file
360393
assets/lexicon/common.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
||||
|
||||
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 @@
|
||||
指令介绍
|
||||
生成二维码 - 将文本内容转换为二维码
|
||||
|
||||
格式
|
||||
生成二维码 <文本内容>
|
||||
|
||||
示例
|
||||
`生成二维码 嗨嗨嗨` 生成扫描结果为“嗨嗨嗨”的二维码图片
|
||||
69
konabot/plugins/gen_qrcode/__init__.py
Normal file
69
konabot/plugins/gen_qrcode/__init__.py
Normal file
@ -0,0 +1,69 @@
|
||||
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
|
||||
|
||||
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"})
|
||||
|
||||
@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())
|
||||
@ -27,6 +27,14 @@ def init_lexicon():
|
||||
# 词语大表
|
||||
with open(ASSETS_PATH / "lexicon" / "ci.json", "r", encoding="utf-8") as f:
|
||||
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:
|
||||
@ -49,7 +57,7 @@ def init_lexicon():
|
||||
ALL_IDIOMS = list(set(ALL_IDIOMS)) # 去重
|
||||
|
||||
# 其他四字词语表,仅表示可以有这个词
|
||||
ALL_WORDS = [word for word in ALL_WORDS if len(word) == 4] + THUOCL_WORDS
|
||||
ALL_WORDS = [word for word in ALL_WORDS if len(word) == 4] + THUOCL_WORDS + COMMON_WORDS
|
||||
ALL_WORDS = list(set(ALL_WORDS)) # 去重
|
||||
|
||||
# 根据成语大表,划分出成语首字字典
|
||||
@ -109,7 +117,7 @@ async def _(event: BaseEvent):
|
||||
# 按分数排序,名字用 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']} 分\n"
|
||||
result_text += f"{i+1}. " + UniMessage().at(user_id) + f": {info['score']} 分\n"
|
||||
await evt.send(await result_text.export())
|
||||
# 重置分数板
|
||||
SCORE_BOARD = {}
|
||||
|
||||
166
konabot/plugins/poll/__init__.py
Normal file
166
konabot/plugins/poll/__init__.py
Normal file
@ -0,0 +1,166 @@
|
||||
import json, time
|
||||
|
||||
from nonebot_plugin_alconna import Alconna, Args, Field, MultiVar, on_alconna
|
||||
from nonebot.adapters.onebot.v11 import Event
|
||||
|
||||
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"})
|
||||
@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={"查看投票"})
|
||||
@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={"投票","参与投票"})
|
||||
@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])
|
||||
@ -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):
|
||||
@ -111,7 +115,11 @@ 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}"
|
||||
@ -128,7 +136,7 @@ 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()
|
||||
@ -214,11 +222,11 @@ async def _():
|
||||
|
||||
DELTA = 2
|
||||
logger.info(f"第一次探测到 Bot 连接,等待 {DELTA} 秒后开始通知")
|
||||
await asyncio.sleep(DELTA)
|
||||
await asynkio.sleep(DELTA)
|
||||
|
||||
await DATA_FILE_LOCK.acquire()
|
||||
|
||||
tasks: set[asyncio.Task[Any]] = set()
|
||||
# tasks: set[asynkio.Task[Any]] = set()
|
||||
cfg = load_notify_config()
|
||||
if cfg.version == 1:
|
||||
logger.info("将配置文件的版本升级为 2")
|
||||
@ -227,11 +235,26 @@ async def _():
|
||||
counter = 0
|
||||
for notify in [*cfg.notifies]:
|
||||
task = create_notify_task(notify, fail2remove=False)
|
||||
tasks.add(task)
|
||||
task.add_done_callback(lambda self: tasks.remove(self))
|
||||
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)
|
||||
|
||||
22
poetry.lock
generated
22
poetry.lock
generated
@ -2518,6 +2518,26 @@ files = [
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "8.2"
|
||||
description = "QR Code image generator"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f"},
|
||||
{file = "qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
all = ["pillow (>=9.1.0)", "pypng"]
|
||||
pil = ["pillow (>=9.1.0)"]
|
||||
png = ["pypng"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
@ -3325,4 +3345,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.12,<4.0"
|
||||
content-hash = "013942994a91012f285305194d73b6609fe71bbd214a63dce443877853dcd764"
|
||||
content-hash = "6fc63a138508a779d47346e0186b4c771ed17b10f278a0e094c6994c1d99a877"
|
||||
|
||||
@ -25,6 +25,7 @@ dependencies = [
|
||||
"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]
|
||||
|
||||
Reference in New Issue
Block a user