Compare commits

...

12 Commits

Author SHA1 Message Date
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
11 changed files with 361049 additions and 71 deletions

View File

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

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

View File

@ -45,7 +45,7 @@
- 帧数必须为正整数(> 0 - 帧数必须为正整数(> 0
- 若原始帧数 ≤ 指定帧数,则保留全部帧。 - 若原始帧数 ≤ 指定帧数,则保留全部帧。
--s <速度>(可选) --speed <速度>(可选)
- 调整 gif 图的速度。若为负数,则代表倒放 - 调整 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: with open(ASSETS_PATH / "lexicon" / "ci.json", "r", encoding="utf-8") as f:
ALL_WORDS = json.load(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 成语库 # 读取 THUOCL 成语库
with open(ASSETS_PATH / "lexicon" / "THUOCL" / "data" / "THUOCL_chengyu.txt", "r", encoding="utf-8") as f: 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_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)) # 去重 ALL_WORDS = list(set(ALL_WORDS)) # 去重
# 根据成语大表,划分出成语首字字典 # 根据成语大表,划分出成语首字字典
@ -67,19 +75,41 @@ LAST_CHAR = ""
USER_NAME_CACHE = {} # 缓存用户名称,避免多次获取 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( evt = on_alconna(Alconna(
"我要玩成语接龙" "我要玩成语接龙"
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True) ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
@evt.handle() @evt.handle()
async def _(event: BaseEvent): async def play_game(event: BaseEvent, force = False):
global NOW_PLAYING, LAST_CHAR, INITED global NOW_PLAYING, LAST_CHAR, INITED
if not INITED:
init_lexicon()
INITED = True
if NOW_PLAYING: if NOW_PLAYING:
await evt.send(await UniMessage().text("当前已有成语接龙游戏在进行中,请稍后再试!").export()) await evt.send(await UniMessage().text("当前已有成语接龙游戏在进行中,请稍后再试!").export())
return 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 NOW_PLAYING = True
await evt.send(await UniMessage().text("你小子,还真有意思!\n好,成语接龙游戏开始!我说一个成语,请大家接下去!").export()) 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()) 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( evt = on_alconna(Alconna(
"不玩了" "不玩了"
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True) ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True)
@evt.handle() @evt.handle()
async def _(event: BaseEvent): async def _(event: BaseEvent):
global NOW_PLAYING, SCORE_BOARD, LAST_CHAR global NOW_PLAYING, SCORE_BOARD, LAST_CHAR, ALL_BUFF_SCORE
if NOW_PLAYING: if NOW_PLAYING:
NOW_PLAYING = False NOW_PLAYING = False
# 发送好吧狗图片 # 发送好吧狗图片
@ -109,7 +148,7 @@ async def _(event: BaseEvent):
# 按分数排序,名字用 at 的方式 # 按分数排序,名字用 at 的方式
sorted_score = sorted(SCORE_BOARD.items(), key=lambda x: x[1]["score"], reverse=True) sorted_score = sorted(SCORE_BOARD.items(), key=lambda x: x[1]["score"], reverse=True)
for i, (user_id, info) in enumerate(sorted_score): 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()) await evt.send(await result_text.export())
# 重置分数板 # 重置分数板
SCORE_BOARD = {} SCORE_BOARD = {}
@ -124,12 +163,11 @@ evt = on_alconna(Alconna(
@evt.handle() @evt.handle()
async def _(event: BaseEvent): async def _(event: BaseEvent):
global NOW_PLAYING, LAST_CHAR global NOW_PLAYING, LAST_CHAR, ALL_BUFF_SCORE
if not NOW_PLAYING: if not NOW_PLAYING:
return return
await evt.send(await UniMessage().text("你们太菜了全部扣100分").export()) await evt.send(await UniMessage().text("你们太菜了全部扣100分").export())
for user_id in SCORE_BOARD: ALL_BUFF_SCORE -= 100
SCORE_BOARD[user_id]["score"] -= 100
# 选择下一个成语 # 选择下一个成语
idiom = secrets.choice(ALL_IDIOMS) idiom = secrets.choice(ALL_IDIOMS)
LAST_CHAR = idiom[-1] LAST_CHAR = idiom[-1]
@ -140,14 +178,36 @@ evt = on_message()
@evt.handle() @evt.handle()
async def _(event: BaseEvent, msg: UniMsg): async def _(event: BaseEvent, msg: UniMsg):
global NOW_PLAYING, LAST_CHAR, SCORE_BOARD global NOW_PLAYING, LAST_CHAR, LOCK
if not NOW_PLAYING: if not NOW_PLAYING:
return return
user_idiom = msg.extract_plain_text().strip() user_idiom = msg.extract_plain_text().strip()
if(user_idiom[0] != LAST_CHAR): if(user_idiom[0] != LAST_CHAR):
return 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): 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 return
# 成功接上 # 成功接上
if isinstance(event, DiscordMessageEvent): if isinstance(event, DiscordMessageEvent):
@ -164,6 +224,6 @@ async def _(event: BaseEvent, msg: UniMsg):
} }
SCORE_BOARD[user_id]["score"] += 1 SCORE_BOARD[user_id]["score"] += 1
# at 指定玩家 # 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] LAST_CHAR = user_idiom[-1]
await evt.send(await UniMessage().text(f"下一个成语请以「{LAST_CHAR}」开头!").export()) await evt.send(await UniMessage().text(f"下一个成语请以「{LAST_CHAR}」开头!").export())

View File

@ -1,16 +1,19 @@
import json, time import json, time
from nonebot_plugin_alconna import (Alconna, Args, Field, MultiVar, UniMessage, from nonebot_plugin_alconna import Alconna, Args, Field, MultiVar, on_alconna
on_alconna)
from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
from nonebot.adapters.onebot.v11 import Event 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_TEMPLATE_FILE = ASSETS_PATH / "json" / "poll.json"
poll_file.close() POLL_DATA_FILE = DATA_PATH / "poll.json"
poll_list = json.loads(poll_list_raw)['poll']
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): async def createpoll(title,qqid,options):
polllength = len(poll_list) polllength = len(poll_list)
@ -44,8 +47,11 @@ def getpolldata(pollid_or_title):
return [thepoll,polnum] return [thepoll,polnum]
def writeback(): def writeback():
file = open(poll_json_path,"w",encoding="utf-8") # file = open(poll_json_path,"w",encoding="utf-8")
json.dump({'poll':poll_list},file,ensure_ascii=False,sort_keys=True) # 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): async def pollvote(polnum,optionnum,qqnum):
optiond = poll_list[polnum]["polldata"] optiond = poll_list[polnum]["polldata"]
@ -157,4 +163,4 @@ async def _(saying: list, event: Event):
# 写入项目 # 写入项目
else: else:
await pollvote(polnum,optionnum,event.get_user_id()) 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 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,7 +115,11 @@ 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}"
@ -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()
@ -214,11 +222,11 @@ async def _():
DELTA = 2 DELTA = 2
logger.info(f"第一次探测到 Bot 连接,等待 {DELTA} 秒后开始通知") logger.info(f"第一次探测到 Bot 连接,等待 {DELTA} 秒后开始通知")
await asyncio.sleep(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") logger.info("将配置文件的版本升级为 2")
@ -227,11 +235,26 @@ async def _():
counter = 0 counter = 0
for notify in [*cfg.notifies]: 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 counter += 1
logger.info(f"成功创建了 {counter} 条代办事项") logger.info(f"成功创建了 {counter} 条代办事项")
save_notify_config(cfg) 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)

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/" url = "https://gitea.service.jazzwhom.top/api/packages/Passthem/pypi/simple/"
priority = "supplemental" priority = "supplemental"
[[tool.poetry.source]]
name = "mirrors"
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
priority = "primary"
[tool.poetry.dependencies] [tool.poetry.dependencies]
ptimeparse = {source = "pt-gitea-pypi"} ptimeparse = {source = "pt-gitea-pypi"}