From c5f820a1f9a43224d59250f8c94e20060a1d2f54 Mon Sep 17 00:00:00 2001 From: WZQ02 Date: Mon, 13 Oct 2025 20:49:56 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=8A=95=E7=A5=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=92=8C=E4=BA=8C=E7=BB=B4=E7=A0=81=E7=94=9F=E6=88=90=EF=BC=88?= =?UTF-8?q?=E4=BB=8E=20testpilot=20=E7=A7=BB=E6=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/json/poll.json | 1 + konabot/plugins/gen_qrcode/__init__.py | 69 +++++++++++ konabot/plugins/poll/__init__.py | 160 +++++++++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 assets/json/poll.json create mode 100644 konabot/plugins/gen_qrcode/__init__.py create mode 100644 konabot/plugins/poll/__init__.py diff --git a/assets/json/poll.json b/assets/json/poll.json new file mode 100644 index 0000000..2f39493 --- /dev/null +++ b/assets/json/poll.json @@ -0,0 +1 @@ +{"poll": {"0": {"create": 1760357553, "expiry": 1760443953, "options": {"0": "此方bot", "1": "testpilot", "2": "小镜bot", "3": "可怜bot"}, "polldata": {}, "qq": "2975499623", "title": "我~是~谁~?"}}} \ No newline at end of file diff --git a/konabot/plugins/gen_qrcode/__init__.py b/konabot/plugins/gen_qrcode/__init__.py new file mode 100644 index 0000000..a65b83b --- /dev/null +++ b/konabot/plugins/gen_qrcode/__init__.py @@ -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()) \ No newline at end of file diff --git a/konabot/plugins/poll/__init__.py b/konabot/plugins/poll/__init__.py new file mode 100644 index 0000000..194f6c0 --- /dev/null +++ b/konabot/plugins/poll/__init__.py @@ -0,0 +1,160 @@ +import json, time + +from nonebot_plugin_alconna import (Alconna, Args, Field, MultiVar, UniMessage, + on_alconna) +from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply +from nonebot.adapters.onebot.v11 import Event + +poll_json_path = "assets/json/poll.json" + +poll_file = open(poll_json_path,"r",encoding="utf-8") +poll_list_raw = poll_file.read() +poll_file.close() +poll_list = json.loads(poll_list_raw)['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) + +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: "参数错误。用法:/poll [投票标题] [选项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("已创建投票。回复 /viewpoll "+str(result)+" 查看该投票。") + else: + await poll.send("投票选项太多了!请减少到15个选项以内。") + +viewpoll = on_alconna(Alconna( + "viewpoll", + Args["saying", MultiVar(str, '+'), Field( + missing_tips=lambda: "请指定投票ID或标题!。用法:/viewpoll [投票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发送 /vote "+str(polnum)+" [选项文本] 即可参与投票!" + await viewpoll.send(reply) + +vote = on_alconna(Alconna( + "vote", + Args["saying", MultiVar(str, '+'), Field( + missing_tips=lambda: "参数错误。用法:/vote [投票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("此投票已经结束!请发送 /viewpoll "+polnum+" 查看结果。") + elif str(event.get_user_id()) in str(thepoll["polldata"]): + await viewpoll.send("你已参与过此投票!请在投票结束后发送 /viewpoll "+polnum+" 查看结果。") + # 写入项目 + else: + await pollvote(polnum,optionnum,event.get_user_id()) + await viewpoll.send("投票成功!你投给了 "+saying[1]) \ No newline at end of file From 4c8625ae0292c189f3a05a4af93f94f12faadda8 Mon Sep 17 00:00:00 2001 From: WZQ02 Date: Mon, 13 Oct 2025 21:08:32 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=B0=8F=E5=AE=8C=E5=96=84=EF=BC=88?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E5=BA=94=E7=9A=84=20man=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/发起投票.txt | 11 +++++++++++ konabot/docs/user/投票.txt | 12 ++++++++++++ konabot/docs/user/查看投票.txt | 12 ++++++++++++ konabot/docs/user/生成二维码.txt | 8 ++++++++ konabot/plugins/poll/__init__.py | 14 +++++++------- 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 konabot/docs/user/发起投票.txt create mode 100644 konabot/docs/user/投票.txt create mode 100644 konabot/docs/user/查看投票.txt create mode 100644 konabot/docs/user/生成二维码.txt diff --git a/konabot/docs/user/发起投票.txt b/konabot/docs/user/发起投票.txt new file mode 100644 index 0000000..d4d3be5 --- /dev/null +++ b/konabot/docs/user/发起投票.txt @@ -0,0 +1,11 @@ +指令介绍 + 发起投票 - 发起一个投票 + +格式 + 发起投票 <投票标题> <选项1> <选项2> ... + +示例 + `发起投票 这是一个投票 A B C` 发起标题为“这是一个投票”,选项为“A”、“B”、“C”的投票 + +说明 + 投票各个选项之间用空格分隔,选项数量为2-15项。投票的默认有效期为24小时。 \ No newline at end of file diff --git a/konabot/docs/user/投票.txt b/konabot/docs/user/投票.txt new file mode 100644 index 0000000..633e70e --- /dev/null +++ b/konabot/docs/user/投票.txt @@ -0,0 +1,12 @@ +指令介绍 + 投票 - 参与已发起的投票 + +格式 + 投票 <投票ID/标题> <选项文本> + +示例 + `投票 1 A` 在ID为1的投票中,投给“A” + `投票 这是一个投票 B` 在标题为“这是一个投票”的投票中,投给“B” + +说明 + 目前不支持单人多投,每个人只能投一项。 \ No newline at end of file diff --git a/konabot/docs/user/查看投票.txt b/konabot/docs/user/查看投票.txt new file mode 100644 index 0000000..f2b53f6 --- /dev/null +++ b/konabot/docs/user/查看投票.txt @@ -0,0 +1,12 @@ +指令介绍 + 查看投票 - 查看已发起的投票 + +格式 + 查看投票 <投票ID或标题> + +示例 + `查看投票 1` 查看ID为1的投票 + `查看投票 这是一个投票` 查看标题为“这是一个投票”的投票 + +说明 + 投票在进行时,使用此命令可以看到投票的各个选项;投票结束后,则可以看到各项的票数。 \ No newline at end of file diff --git a/konabot/docs/user/生成二维码.txt b/konabot/docs/user/生成二维码.txt new file mode 100644 index 0000000..42ad4cf --- /dev/null +++ b/konabot/docs/user/生成二维码.txt @@ -0,0 +1,8 @@ +指令介绍 + 生成二维码 - 将文本内容转换为二维码 + +格式 + 生成二维码 <文本内容> + +示例 + `生成二维码 嗨嗨嗨` 生成扫描结果为“嗨嗨嗨”的二维码图片 \ No newline at end of file diff --git a/konabot/plugins/poll/__init__.py b/konabot/plugins/poll/__init__.py index 194f6c0..266a1c6 100644 --- a/konabot/plugins/poll/__init__.py +++ b/konabot/plugins/poll/__init__.py @@ -58,7 +58,7 @@ async def pollvote(polnum,optionnum,qqnum): poll = on_alconna(Alconna( "poll", Args["saying", MultiVar(str, '+'), Field( - missing_tips=lambda: "参数错误。用法:/poll [投票标题] [选项1] [选项2]" + missing_tips=lambda: "参数错误。用法:发起投票 <投票标题> <选项1> <选项2> ..." )], ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"发起投票","createpoll"}) @poll.handle() @@ -73,14 +73,14 @@ async def _(saying: list, event: Event): options[saying.index(i)] = i qqid = event.get_user_id() result = await createpoll(title,qqid,options) - await poll.send("已创建投票。回复 /viewpoll "+str(result)+" 查看该投票。") + await poll.send("已创建投票。回复 查看投票 "+str(result)+" 查看该投票。") else: await poll.send("投票选项太多了!请减少到15个选项以内。") viewpoll = on_alconna(Alconna( "viewpoll", Args["saying", MultiVar(str, '+'), Field( - missing_tips=lambda: "请指定投票ID或标题!。用法:/viewpoll [投票ID/标题]" + missing_tips=lambda: "请指定投票ID或标题!。用法:查看投票 <投票ID或标题>" )], ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"查看投票"}) @viewpoll.handle() @@ -116,13 +116,13 @@ async def _(saying: list): reply += "\n" reply += "- "+thepoll["options"][i] # reply += "\n\n小提示:向bot私聊发送 /viewpoll "+str(polnum)+" 可查看已投票数哦!" - reply += "\n\n发送 /vote "+str(polnum)+" [选项文本] 即可参与投票!" + reply += "\n\n发送 投票 "+str(polnum)+" <选项文本> 即可参与投票!" await viewpoll.send(reply) vote = on_alconna(Alconna( "vote", Args["saying", MultiVar(str, '+'), Field( - missing_tips=lambda: "参数错误。用法:/vote [投票ID/标题] [选项文本]" + missing_tips=lambda: "参数错误。用法:投票 <投票ID/标题> <选项文本>" )], ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"投票","参与投票"}) @vote.handle() @@ -151,9 +151,9 @@ async def _(saying: list, event: Event): await viewpoll.send(reply) # 检查是否符合投票条件(该qq号是否已参与过投票、投票是否过期) elif time.time() > thepoll["expiry"]: - await viewpoll.send("此投票已经结束!请发送 /viewpoll "+polnum+" 查看结果。") + await viewpoll.send("此投票已经结束!请发送 查看投票 "+polnum+" 查看结果。") elif str(event.get_user_id()) in str(thepoll["polldata"]): - await viewpoll.send("你已参与过此投票!请在投票结束后发送 /viewpoll "+polnum+" 查看结果。") + await viewpoll.send("你已参与过此投票!请在投票结束后发送 查看投票 "+polnum+" 查看结果。") # 写入项目 else: await pollvote(polnum,optionnum,event.get_user_id()) From a208302cb998a2a71c45679912b3a31ea0d210bc Mon Sep 17 00:00:00 2001 From: passthem Date: Mon, 13 Oct 2025 21:35:44 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 22 +++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 9b385b8..1629218 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 906e987..4c6a7a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]