Compare commits

..

13 Commits

Author SHA1 Message Date
4f885554ca 添加卵总展示
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-10-16 22:29:07 +08:00
7ebcb8add4 Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-10-16 19:13:51 +08:00
e18cc82792 修复 av/bv 号无法直接被筛选读取的问题 2025-10-16 19:13:36 +08:00
eb28cd0a0c 更正 Giftool 错误的文档 2025-10-16 18:44:22 +08:00
2d688a6ed6 new
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-10-14 12:43:25 +08:00
e9aac52200 chengyu update 2025-10-14 01:23:49 +00:00
4305548ab5 submodule
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-10-13 22:53:44 +08:00
99382a3bf5 Merge branch 'master' of https://gitea.service.jazzwhom.top/mttu-developers/konabot
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-13 22:48:17 +08:00
92e43785bf submodule 2025-10-13 22:46:30 +08:00
fc5b11c5e8 调整 notify 的强制退出
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-10-13 22:16:50 +08:00
0ec66988fa 更新投票存储位置
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-13 22:05:21 +08:00
e5c3081c22 Merge branch 'master' of https://gitea.service.jazzwhom.top/mttu-developers/konabot
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-13 22:02:44 +08:00
14b356120a 成语接龙 2025-10-13 22:02:33 +08:00
15 changed files with 361139 additions and 75 deletions

View File

@ -10,6 +10,10 @@ trigger:
- master
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: 构建 Docker 镜像
image: plugins/docker:latest
privileged: true
@ -50,6 +54,10 @@ trigger:
- tag
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: 构建并推送 Release Docker 镜像
image: plugins/docker:latest
privileged: true

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

360393
assets/lexicon/common.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -45,7 +45,7 @@
- 帧数必须为正整数(> 0
- 若原始帧数 ≤ 指定帧数,则保留全部帧。
--s <速度>(可选)
--speed <速度>(可选)
- 调整 gif 图的速度。若为负数,则代表倒放
使用方式

View File

@ -0,0 +1,39 @@
import re
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小程序(?:&amp;#93;|&#93;|\])哔哩哔哩).{0,500}"
)
@matcher_fix.handle()
async def _(msg: UniMsg, event: Event):
to_search = msg.exclude(Reply, Reference).dump(json=True)
to_search2 = msg.exclude(Reply, Reference).extract_plain_text()
if not re.search(pattern, to_search) and not re.search(pattern, to_search2):
return
from nonebot_plugin_analysis_bilibili import handle_analysis
await handle_analysis(event)
# b_url: str
# b_page: str | None
# b_time: str | None
#
# from nonebot_plugin_analysis_bilibili.analysis_bilibili import extract as bilibili_extract
#
# b_url, b_page, b_time = bilibili_extract(to_search)
# if b_url is None:
# return
#
# await matcher_fix.send(await UniMessage().text(b_url).export())

View File

@ -1,25 +0,0 @@
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小程序(?:&amp;#93;|&#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)

View File

@ -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)) # 去重
# 根据成语大表,划分出成语首字字典
@ -67,19 +75,41 @@ LAST_CHAR = ""
USER_NAME_CACHE = {} # 缓存用户名称,避免多次获取
REMAIN_PLAYING_TIMES = 1
LAST_PLAY_DATE = ""
LOCK = False
ALL_BUFF_SCORE = 0 # 全体分数
import datetime
def be_able_to_play():
global REMAIN_PLAYING_TIMES, LAST_PLAY_DATE
if(LAST_PLAY_DATE != datetime.date.today()):
LAST_PLAY_DATE = datetime.date.today()
REMAIN_PLAYING_TIMES = 1
if(REMAIN_PLAYING_TIMES > 0):
REMAIN_PLAYING_TIMES -= 1
return True
return False
evt = on_alconna(Alconna(
"我要玩成语接龙"
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
@evt.handle()
async def _(event: BaseEvent):
async def play_game(event: BaseEvent, force = False):
global NOW_PLAYING, LAST_CHAR, INITED
if not INITED:
init_lexicon()
INITED = True
if NOW_PLAYING:
await evt.send(await UniMessage().text("当前已有成语接龙游戏在进行中,请稍后再试!").export())
return
if not be_able_to_play() and not force:
await evt.send(await UniMessage().text("玩玩玩,就知道玩,快去睡觉!").export())
return
if not INITED:
init_lexicon()
INITED = True
NOW_PLAYING = True
await evt.send(await UniMessage().text("你小子,还真有意思!\n好,成语接龙游戏开始!我说一个成语,请大家接下去!").export())
# 选择一个随机成语
@ -88,13 +118,22 @@ async def _(event: BaseEvent):
# 发布成语
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 force_play_game(event: BaseEvent):
await play_game(event, force=True)
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
global NOW_PLAYING, SCORE_BOARD, LAST_CHAR, ALL_BUFF_SCORE
if NOW_PLAYING:
NOW_PLAYING = False
# 发送好吧狗图片
@ -109,7 +148,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'] + ALL_BUFF_SCORE}\n"
await evt.send(await result_text.export())
# 重置分数板
SCORE_BOARD = {}
@ -124,12 +163,11 @@ evt = on_alconna(Alconna(
@evt.handle()
async def _(event: BaseEvent):
global NOW_PLAYING, LAST_CHAR
global NOW_PLAYING, LAST_CHAR, ALL_BUFF_SCORE
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
ALL_BUFF_SCORE -= 100
# 选择下一个成语
idiom = secrets.choice(ALL_IDIOMS)
LAST_CHAR = idiom[-1]
@ -140,14 +178,36 @@ evt = on_message()
@evt.handle()
async def _(event: BaseEvent, msg: UniMsg):
global NOW_PLAYING, LAST_CHAR, SCORE_BOARD
global NOW_PLAYING, LAST_CHAR, LOCK
if not NOW_PLAYING:
return
user_idiom = msg.extract_plain_text().strip()
if(user_idiom[0] != LAST_CHAR):
return
if LOCK:
return
LOCK = True
await handle_send_info(event, msg)
LOCK = False
async def handle_send_info(event: BaseEvent, msg: UniMsg):
global NOW_PLAYING, LAST_CHAR, SCORE_BOARD, ALL_BUFF_SCORE
user_idiom = msg.extract_plain_text().strip()
if(user_idiom not in ALL_IDIOMS and user_idiom not in ALL_WORDS):
await evt.send(await UniMessage().text("接不上!这个不一样!").export())
# 扣0.1分
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"] -= 0.1
await evt.send(await UniMessage().at(user_id).text("接不上!这个不一样!你被扣了 0.1 分!").export())
return
# 成功接上
if isinstance(event, DiscordMessageEvent):
@ -164,6 +224,6 @@ async def _(event: BaseEvent, msg: UniMsg):
}
SCORE_BOARD[user_id]["score"] += 1
# at 指定玩家
await evt.send(await UniMessage().at(user_id).text(f"接对了!你有 {SCORE_BOARD[user_id]['score']} 分!").export())
await evt.send(await UniMessage().at(user_id).text(f"接对了!你有 {SCORE_BOARD[user_id]['score'] + ALL_BUFF_SCORE} 分!").export())
LAST_CHAR = user_idiom[-1]
await evt.send(await UniMessage().text(f"下一个成语请以「{LAST_CHAR}」开头!").export())

View File

@ -2,11 +2,11 @@ from io import BytesIO
from typing import Iterable, cast
from nonebot import on_message
from nonebot_plugin_alconna import (Alconna, Args, Field, MultiVar, Text,
from nonebot_plugin_alconna import (Alconna, Args, Field, Image, MultiVar, Text,
UniMessage, UniMsg, on_alconna)
from konabot.common.nb.extract_image import extract_image_from_message
from konabot.plugins.memepack.drawing.display import draw_cao_display
from konabot.common.nb.extract_image import PIL_Image, extract_image_from_message
from konabot.plugins.memepack.drawing.display import draw_cao_display, draw_snaur_display
from konabot.plugins.memepack.drawing.saying import (draw_cute_ten,
draw_geimao, draw_mnk,
draw_pt, draw_suan)
@ -139,3 +139,17 @@ async def _(msg: UniMsg, evt: Event, bot: Bot):
.text(err)
.export()
)
snaur_display_cmd = on_alconna(Alconna(
"卵总展示",
Args["image", Image | None],
))
@snaur_display_cmd.handle()
async def _(img: PIL_Image):
img_processed = await draw_snaur_display(img)
img_data = BytesIO()
img_processed.save(img_data, "PNG")
await snaur_display_cmd.send(await UniMessage().image(raw=img_data).export())

View File

@ -4,10 +4,12 @@ from typing import Any, cast
import cv2
import numpy as np
import PIL.Image
import PIL.ImageChops
import PIL.ImageEnhance
from konabot.common.path import ASSETS_PATH
cao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "caoimg1.png")
cao_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "caoimg1.png")
CAO_QUAD_POINTS = np.float32(cast(Any, [
[392, 540],
[577, 557],
@ -15,6 +17,16 @@ CAO_QUAD_POINTS = np.float32(cast(Any, [
[381, 687],
]))
snaur_image_base = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "snaur_1_base.png")
snaur_image_top = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "snaur_1_top.png")
SNAUR_RATIO = (1 / 2) ** .5
SNAUR_QUAD_POINTS = np.float32(cast(Any, [
[0, 466 ],
[673, 471 ],
[640, 1196],
[106, 1280],
]))
def _draw_cao_display(image: PIL.Image.Image):
src = np.array(image.convert("RGB"))
h, w = src.shape[:2]
@ -43,3 +55,63 @@ def _draw_cao_display(image: PIL.Image.Image):
async def draw_cao_display(image: PIL.Image.Image):
return await asyncio.to_thread(_draw_cao_display, image)
def _draw_snaur_display(image: PIL.Image.Image):
src = np.array(image.convert("RGB"))
_h, _w = src.shape[:2]
if _w / _h < SNAUR_RATIO:
_w_target = _w
_h_target = int(_w / SNAUR_RATIO)
else:
_w_target = int(_h * SNAUR_RATIO)
_h_target = _h
x_center = _w / 2
y_center = _h / 2
x1 = int(x_center - _w_target / 2)
x2 = int(x_center + _w_target / 2)
y1 = int(y_center - _h_target / 2)
y2 = int(y_center + _h_target / 2)
src = src[y1:y2, x1:x2, :]
h, w = src.shape[:2]
src_points = np.float32(cast(Any, [
[0, 0],
[w, 0],
[w, h],
[0, h],
]))
dst_points = SNAUR_QUAD_POINTS
M = cv2.getPerspectiveTransform(cast(Any, src_points), cast(Any, dst_points))
output_size = snaur_image_top.size
output_w, output_h = output_size
warped = cv2.warpPerspective(
src,
M,
(output_w, output_h),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0, 0, 0)
)
result = PIL.Image.fromarray(warped, 'RGB').convert('RGBA')
result = PIL.ImageChops.multiply(result, snaur_image_base)
r, g, b, a = result.split()
a = a.point(lambda p: int(p * 0.8))
result = PIL.Image.merge('RGBA', (r, g, b, a))
enhancer = PIL.ImageEnhance.Color(result)
result = enhancer.enhance(0.85)
result = PIL.Image.alpha_composite(result, snaur_image_top)
return result
async def draw_snaur_display(image: PIL.Image.Image) -> PIL.Image.Image:
return await asyncio.to_thread(_draw_snaur_display, image)

View File

@ -1,16 +1,19 @@
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_plugin_alconna import Alconna, Args, Field, MultiVar, on_alconna
from nonebot.adapters.onebot.v11 import Event
poll_json_path = "assets/json/poll.json"
from konabot.common.path import ASSETS_PATH, DATA_PATH
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']
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)
@ -44,8 +47,11 @@ def getpolldata(pollid_or_title):
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)
# 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"]
@ -157,4 +163,4 @@ async def _(saying: list, event: Event):
# 写入项目
else:
await pollvote(polnum,optionnum,event.get_user_id())
await viewpoll.send("投票成功!你投给了 "+saying[1])
await viewpoll.send("投票成功!你投给了 "+saying[1])

View File

@ -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)

486
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -37,5 +37,10 @@ name = "pt-gitea-pypi"
url = "https://gitea.service.jazzwhom.top/api/packages/Passthem/pypi/simple/"
priority = "supplemental"
[[tool.poetry.source]]
name = "mirrors"
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
priority = "primary"
[tool.poetry.dependencies]
ptimeparse = {source = "pt-gitea-pypi"}