Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4305548ab5 | |||
| 99382a3bf5 | |||
| 92e43785bf | |||
| fc5b11c5e8 | |||
| 0ec66988fa | |||
| e5c3081c22 | |||
| 14b356120a | |||
| a208302cb9 | |||
| 01ffa451bb | |||
| 2b6c2e84bd | |||
| 4f0a9af2dc | |||
| 4a4aa6b243 | |||
| 4c8625ae02 | |||
| c5f820a1f9 | |||
| a3dd2dbbda | |||
| 8d4f74dafe | |||
| 7c1bac64c9 | |||
| e09fa13d0f |
@ -10,6 +10,10 @@ trigger:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: submodules
|
||||||
|
image: alpine/git
|
||||||
|
commands:
|
||||||
|
- git submodule update --init --recursive
|
||||||
- name: 构建 Docker 镜像
|
- name: 构建 Docker 镜像
|
||||||
image: plugins/docker:latest
|
image: plugins/docker:latest
|
||||||
privileged: true
|
privileged: true
|
||||||
@ -50,6 +54,10 @@ trigger:
|
|||||||
- tag
|
- tag
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: submodules
|
||||||
|
image: alpine/git
|
||||||
|
commands:
|
||||||
|
- git submodule update --init --recursive
|
||||||
- name: 构建并推送 Release Docker 镜像
|
- name: 构建并推送 Release Docker 镜像
|
||||||
image: plugins/docker:latest
|
image: plugins/docker:latest
|
||||||
privileged: true
|
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 |
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_builtin_plugin("echo")
|
||||||
nonebot.load_plugins("konabot/plugins")
|
nonebot.load_plugins("konabot/plugins")
|
||||||
|
nonebot.load_plugin("nonebot_plugin_analysis_bilibili")
|
||||||
|
|
||||||
nonebot.run()
|
nonebot.run()
|
||||||
|
|
||||||
|
|||||||
@ -12,3 +12,10 @@ DOCS_PATH_MAN1 = DOCS_PATH / "user"
|
|||||||
DOCS_PATH_MAN3 = DOCS_PATH / "lib"
|
DOCS_PATH_MAN3 = DOCS_PATH / "lib"
|
||||||
DOCS_PATH_MAN7 = DOCS_PATH / "concepts"
|
DOCS_PATH_MAN7 = DOCS_PATH / "concepts"
|
||||||
DOCS_PATH_MAN8 = DOCS_PATH / "sys"
|
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 @@
|
|||||||
|
指令介绍
|
||||||
|
生成二维码 - 将文本内容转换为二维码
|
||||||
|
|
||||||
|
格式
|
||||||
|
生成二维码 <文本内容>
|
||||||
|
|
||||||
|
示例
|
||||||
|
`生成二维码 嗨嗨嗨` 生成扫描结果为“嗨嗨嗨”的二维码图片
|
||||||
25
konabot/plugins/bilibili_fetch_fix.py
Normal file
25
konabot/plugins/bilibili_fetch_fix.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import re
|
||||||
|
from loguru import logger
|
||||||
|
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)
|
||||||
|
if not re.search(pattern, to_search):
|
||||||
|
return
|
||||||
|
logger.info("检测到有 Bilibili 相关的消息,直接进行一个调用")
|
||||||
|
_module = __import__("nonebot_plugin_analysis_bilibili")
|
||||||
|
await _module.handle_analysis(event)
|
||||||
|
|
||||||
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())
|
||||||
177
konabot/plugins/idiomgame/__init__.py
Normal file
177
konabot/plugins/idiomgame/__init__.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import base64
|
||||||
|
import secrets
|
||||||
|
import json
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from nonebot import on_message
|
||||||
|
from nonebot.adapters import Event as BaseEvent
|
||||||
|
from nonebot.adapters.console.event import MessageEvent as ConsoleMessageEvent
|
||||||
|
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
|
||||||
|
from nonebot_plugin_alconna import (Alconna, Args, Field, Subcommand,
|
||||||
|
UniMessage, UniMsg, on_alconna)
|
||||||
|
|
||||||
|
from konabot.common.path import ASSETS_PATH
|
||||||
|
|
||||||
|
ALL_WORDS = [] # 所有四字词语
|
||||||
|
ALL_IDIOMS = [] # 所有成语
|
||||||
|
IDIOM_FIRST_CHAR = {} # 成语首字字典
|
||||||
|
|
||||||
|
INITED = False
|
||||||
|
|
||||||
|
def init_lexicon():
|
||||||
|
global ALL_WORDS, ALL_IDIOMS, IDIOM_FIRST_CHAR
|
||||||
|
# 成语大表
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
# 只有成语的大表
|
||||||
|
ALL_IDIOMS = [idiom["word"] for idiom in ALL_IDIOMS_INFOS] + THUOCL_IDIOMS
|
||||||
|
ALL_IDIOMS = list(set(ALL_IDIOMS)) # 去重
|
||||||
|
|
||||||
|
# 其他四字词语表,仅表示可以有这个词
|
||||||
|
ALL_WORDS = [word for word in ALL_WORDS if len(word) == 4] + THUOCL_WORDS + COMMON_WORDS
|
||||||
|
ALL_WORDS = list(set(ALL_WORDS)) # 去重
|
||||||
|
|
||||||
|
# 根据成语大表,划分出成语首字字典
|
||||||
|
IDIOM_FIRST_CHAR = {}
|
||||||
|
for idiom in ALL_IDIOMS + ALL_WORDS:
|
||||||
|
if idiom[0] not in IDIOM_FIRST_CHAR:
|
||||||
|
IDIOM_FIRST_CHAR[idiom[0]] = []
|
||||||
|
IDIOM_FIRST_CHAR[idiom[0]].append(idiom)
|
||||||
|
|
||||||
|
NOW_PLAYING = False
|
||||||
|
|
||||||
|
SCORE_BOARD = {}
|
||||||
|
|
||||||
|
LAST_CHAR = ""
|
||||||
|
|
||||||
|
USER_NAME_CACHE = {} # 缓存用户名称,避免多次获取
|
||||||
|
|
||||||
|
evt = on_alconna(Alconna(
|
||||||
|
"我要玩成语接龙"
|
||||||
|
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent):
|
||||||
|
global NOW_PLAYING, LAST_CHAR, INITED
|
||||||
|
if not INITED:
|
||||||
|
init_lexicon()
|
||||||
|
INITED = True
|
||||||
|
if NOW_PLAYING:
|
||||||
|
await evt.send(await UniMessage().text("当前已有成语接龙游戏在进行中,请稍后再试!").export())
|
||||||
|
return
|
||||||
|
NOW_PLAYING = True
|
||||||
|
await evt.send(await UniMessage().text("你小子,还真有意思!\n好,成语接龙游戏开始!我说一个成语,请大家接下去!").export())
|
||||||
|
# 选择一个随机成语
|
||||||
|
idiom = secrets.choice(ALL_IDIOMS)
|
||||||
|
LAST_CHAR = idiom[-1]
|
||||||
|
# 发布成语
|
||||||
|
await evt.send(await UniMessage().text(f"第一个成语:「{idiom}」,请接!").export())
|
||||||
|
|
||||||
|
evt = on_alconna(Alconna(
|
||||||
|
"不玩了"
|
||||||
|
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent):
|
||||||
|
global NOW_PLAYING, SCORE_BOARD, LAST_CHAR
|
||||||
|
if NOW_PLAYING:
|
||||||
|
NOW_PLAYING = False
|
||||||
|
# 发送好吧狗图片
|
||||||
|
# 打开好吧狗本地文件
|
||||||
|
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())
|
||||||
|
result_text = UniMessage().text("游戏结束!\n最终得分榜:\n")
|
||||||
|
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']} 分\n"
|
||||||
|
await evt.send(await result_text.export())
|
||||||
|
# 重置分数板
|
||||||
|
SCORE_BOARD = {}
|
||||||
|
LAST_CHAR = ""
|
||||||
|
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 _(event: BaseEvent):
|
||||||
|
global NOW_PLAYING, LAST_CHAR
|
||||||
|
if not NOW_PLAYING:
|
||||||
|
return
|
||||||
|
await evt.send(await UniMessage().text("你们太菜了!全部扣100分!").export())
|
||||||
|
for user_id in SCORE_BOARD:
|
||||||
|
SCORE_BOARD[user_id]["score"] -= 100
|
||||||
|
# 选择下一个成语
|
||||||
|
idiom = secrets.choice(ALL_IDIOMS)
|
||||||
|
LAST_CHAR = idiom[-1]
|
||||||
|
await evt.send(await UniMessage().text(f"重新开始,下一个成语是「{idiom}」").export())
|
||||||
|
|
||||||
|
# 直接读取消息
|
||||||
|
evt = on_message()
|
||||||
|
|
||||||
|
@evt.handle()
|
||||||
|
async def _(event: BaseEvent, msg: UniMsg):
|
||||||
|
global NOW_PLAYING, LAST_CHAR, SCORE_BOARD
|
||||||
|
if not NOW_PLAYING:
|
||||||
|
return
|
||||||
|
user_idiom = msg.extract_plain_text().strip()
|
||||||
|
if(user_idiom[0] != LAST_CHAR):
|
||||||
|
return
|
||||||
|
if(user_idiom not in ALL_IDIOMS and user_idiom not in ALL_WORDS):
|
||||||
|
await evt.send(await UniMessage().text("接不上!这个不一样!").export())
|
||||||
|
return
|
||||||
|
# 成功接上
|
||||||
|
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())
|
||||||
|
|
||||||
|
if user_id not in SCORE_BOARD:
|
||||||
|
SCORE_BOARD[user_id] = {
|
||||||
|
"name": user_name,
|
||||||
|
"score": 0
|
||||||
|
}
|
||||||
|
SCORE_BOARD[user_id]["score"] += 1
|
||||||
|
# at 指定玩家
|
||||||
|
await evt.send(await UniMessage().at(user_id).text(f"接对了!你有 {SCORE_BOARD[user_id]['score']} 分!").export())
|
||||||
|
LAST_CHAR = user_idiom[-1]
|
||||||
|
await evt.send(await UniMessage().text(f"下一个成语请以「{LAST_CHAR}」开头!").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"
|
||||||
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])
|
||||||
@ -309,7 +309,7 @@ async def generate_dice_image(number: str) -> BytesIO:
|
|||||||
if(len(text) > 50):
|
if(len(text) > 50):
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
push_image = Image.open(ASSETS_PATH / "img" / "dice" / "stick.png")
|
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)
|
output.seek(0)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio as asynkio
|
||||||
import datetime
|
import datetime
|
||||||
|
import functools
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, cast
|
from typing import Any, Literal, cast
|
||||||
|
|
||||||
|
import signal
|
||||||
import nonebot
|
import nonebot
|
||||||
import ptimeparse
|
import ptimeparse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@ -24,7 +26,9 @@ evt = on_message()
|
|||||||
|
|
||||||
(Path(__file__).parent.parent.parent.parent / "data").mkdir(exist_ok=True)
|
(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_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):
|
class Notify(BaseModel):
|
||||||
@ -111,11 +115,15 @@ def create_notify_task(notify: Notify, fail2remove: bool = True):
|
|||||||
async def mission():
|
async def mission():
|
||||||
begin_time = datetime.datetime.now()
|
begin_time = datetime.datetime.now()
|
||||||
if begin_time < notify.notify_time:
|
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:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"期望在 {notify.notify_time} 在平台 {notify.platform} {notify.target_env}"
|
f"期望在 {notify.notify_time} 在平台 {notify.platform} {notify.target_env}"
|
||||||
f"{notify.target} 的代办通知 {notify.notify_msg} 已经超时,将会直接通知!"
|
f" {notify.target} 的代办通知 {notify.notify_msg} 已经超时,将会直接通知!"
|
||||||
)
|
)
|
||||||
res = await notify_now(notify)
|
res = await notify_now(notify)
|
||||||
if fail2remove or res:
|
if fail2remove or res:
|
||||||
@ -128,7 +136,7 @@ def create_notify_task(notify: Notify, fail2remove: bool = True):
|
|||||||
DATA_FILE_LOCK.release()
|
DATA_FILE_LOCK.release()
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
return asyncio.create_task(mission())
|
return asynkio.create_task(mission())
|
||||||
|
|
||||||
|
|
||||||
@evt.handle()
|
@evt.handle()
|
||||||
@ -148,6 +156,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
|||||||
# target_time = get_target_time(notify_time)
|
# target_time = get_target_time(notify_time)
|
||||||
try:
|
try:
|
||||||
target_time = ptimeparse.parse(notify_time)
|
target_time = ptimeparse.parse(notify_time)
|
||||||
|
logger.info(f"从 {notify_time} 解析出了时间:{target_time}")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.info(f"无法从 {notify_time} 中解析出时间")
|
logger.info(f"无法从 {notify_time} 中解析出时间")
|
||||||
return
|
return
|
||||||
@ -159,7 +168,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
|||||||
|
|
||||||
await DATA_FILE_LOCK.acquire()
|
await DATA_FILE_LOCK.acquire()
|
||||||
cfg = load_notify_config()
|
cfg = load_notify_config()
|
||||||
|
|
||||||
if isinstance(mEvt, ConsoleMessageEvent):
|
if isinstance(mEvt, ConsoleMessageEvent):
|
||||||
platform = "console"
|
platform = "console"
|
||||||
target = mEvt.get_user_id()
|
target = mEvt.get_user_id()
|
||||||
@ -186,7 +195,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
|||||||
notify_time=target_time,
|
notify_time=target_time,
|
||||||
notify_msg=notify_text,
|
notify_msg=notify_text,
|
||||||
)
|
)
|
||||||
await create_notify_task(notify)
|
create_notify_task(notify)
|
||||||
|
|
||||||
cfg.notifies.append(notify)
|
cfg.notifies.append(notify)
|
||||||
save_notify_config(cfg)
|
save_notify_config(cfg)
|
||||||
@ -194,6 +203,7 @@ async def _(msg: UniMsg, mEvt: Event):
|
|||||||
|
|
||||||
await evt.send(await UniMessage().at(mEvt.get_user_id()).text(
|
await evt.send(await UniMessage().at(mEvt.get_user_id()).text(
|
||||||
f" 了解啦!将会在 {notify.notify_time} 提醒你哦~").export())
|
f" 了解啦!将会在 {notify.notify_time} 提醒你哦~").export())
|
||||||
|
logger.info(f"创建了一条于 {notify.notify_time} 的代办提醒")
|
||||||
|
|
||||||
|
|
||||||
driver = nonebot.get_driver()
|
driver = nonebot.get_driver()
|
||||||
@ -209,20 +219,42 @@ async def _():
|
|||||||
return
|
return
|
||||||
|
|
||||||
NOTIFIED_FLAG["task_added"] = True
|
NOTIFIED_FLAG["task_added"] = True
|
||||||
logger.info("第一次探测到 Bot 连接,等待 10 秒后开始通知")
|
|
||||||
|
|
||||||
await asyncio.sleep(10)
|
DELTA = 2
|
||||||
|
logger.info(f"第一次探测到 Bot 连接,等待 {DELTA} 秒后开始通知")
|
||||||
|
await asynkio.sleep(DELTA)
|
||||||
|
|
||||||
await DATA_FILE_LOCK.acquire()
|
await DATA_FILE_LOCK.acquire()
|
||||||
|
|
||||||
tasks: set[asyncio.Task[Any]] = set()
|
# tasks: set[asynkio.Task[Any]] = set()
|
||||||
cfg = load_notify_config()
|
cfg = load_notify_config()
|
||||||
if cfg.version == 1:
|
if cfg.version == 1:
|
||||||
|
logger.info("将配置文件的版本升级为 2")
|
||||||
cfg.version = 2
|
cfg.version = 2
|
||||||
else:
|
else:
|
||||||
for notify in cfg.notifies:
|
counter = 0
|
||||||
|
for notify in [*cfg.notifies]:
|
||||||
task = create_notify_task(notify, fail2remove=False)
|
task = create_notify_task(notify, fail2remove=False)
|
||||||
tasks.add(task)
|
ASYNK_TASKS.add(task)
|
||||||
task.add_done_callback(lambda self: tasks.remove(self))
|
task.add_done_callback(lambda self: ASYNK_TASKS.remove(self))
|
||||||
|
counter += 1
|
||||||
|
logger.info(f"成功创建了 {counter} 条代办事项")
|
||||||
|
save_notify_config(cfg)
|
||||||
DATA_FILE_LOCK.release()
|
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)
|
||||||
|
|||||||
87
poetry.lock
generated
87
poetry.lock
generated
@ -748,6 +748,18 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>
|
|||||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetype"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"},
|
||||||
|
{file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "frozenlist"
|
name = "frozenlist"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
@ -1630,6 +1642,23 @@ nonebot-plugin-waiter = ">=0.6.0"
|
|||||||
nonebot2 = ">=2.3.0"
|
nonebot2 = ">=2.3.0"
|
||||||
tarina = ">=0.6.8,<0.7"
|
tarina = ">=0.6.8,<0.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nonebot-plugin-analysis-bilibili"
|
||||||
|
version = "2.8.1"
|
||||||
|
description = "nonebot2解析bilibili插件"
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4.0,>=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "nonebot_plugin_analysis_bilibili-2.8.1-py3-none-any.whl", hash = "sha256:f882a0428ca87fc77a56fae6013607f6b286d8ae0ed46a1a87b4a1a6d3f4d011"},
|
||||||
|
{file = "nonebot_plugin_analysis_bilibili-2.8.1.tar.gz", hash = "sha256:17c2c15a1783a2e1075638861384bd55b3fef09c0c7eb94857f811f60dbb446a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiohttp = ">=3.7,<4.0"
|
||||||
|
nonebot-plugin-send-anything-anywhere = ">=0,<1"
|
||||||
|
nonebot2 = ">=2.1.1,<3.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonebot-plugin-apscheduler"
|
name = "nonebot-plugin-apscheduler"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1647,6 +1676,25 @@ apscheduler = ">=3.7.0,<4.0.0"
|
|||||||
nonebot2 = ">=2.2.0,<3.0.0"
|
nonebot2 = ">=2.2.0,<3.0.0"
|
||||||
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
|
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nonebot-plugin-send-anything-anywhere"
|
||||||
|
version = "0.7.1"
|
||||||
|
description = "An adaptor for nonebot2 adaptors"
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4.0,>=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "nonebot_plugin_send_anything_anywhere-0.7.1-py3-none-any.whl", hash = "sha256:b52044272be9a7bc77bd7a53ef700481c071fd4e889ae21a0411d0df22d13c16"},
|
||||||
|
{file = "nonebot_plugin_send_anything_anywhere-0.7.1.tar.gz", hash = "sha256:ec9c7ba59c824238b812950146a894acc88ca8da98f500de15217feebcd5e868"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = ">=3.3.0,<5.0.0"
|
||||||
|
filetype = ">=1.2.0,<2.0.0"
|
||||||
|
nonebot2 = ">=2.3.0,<3.0.0"
|
||||||
|
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
|
||||||
|
strenum = ">=0.4.8,<0.5.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonebot-plugin-waiter"
|
name = "nonebot-plugin-waiter"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@ -2470,6 +2518,26 @@ files = [
|
|||||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
{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]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
@ -2620,6 +2688,23 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strenum"
|
||||||
|
version = "0.4.15"
|
||||||
|
description = "An Enum that inherits from str."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"},
|
||||||
|
{file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"]
|
||||||
|
release = ["twine"]
|
||||||
|
test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tarina"
|
name = "tarina"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@ -3260,4 +3345,4 @@ type = ["pytest-mypy"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.12,<4.0"
|
python-versions = ">=3.12,<4.0"
|
||||||
content-hash = "0c7709438b8eb3d468b775417b8ef642cd7a8c1031805fdc71fec32c48346e54"
|
content-hash = "6fc63a138508a779d47346e0186b4c771ed17b10f278a0e094c6994c1d99a877"
|
||||||
|
|||||||
@ -24,6 +24,8 @@ dependencies = [
|
|||||||
"returns (>=0.26.0,<0.27.0)",
|
"returns (>=0.26.0,<0.27.0)",
|
||||||
"ptimeparse (>=0.1.1,<0.2.0)",
|
"ptimeparse (>=0.1.1,<0.2.0)",
|
||||||
"skia-python (>=138.0,<139.0)",
|
"skia-python (>=138.0,<139.0)",
|
||||||
|
"nonebot-plugin-analysis-bilibili (>=2.8.1,<3.0.0)",
|
||||||
|
"qrcode (>=8.2,<9.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
Reference in New Issue
Block a user