Compare commits

...

5 Commits

Author SHA1 Message Date
XuChenXu
b0e84c778b 🔖 Version 2.7.0 2025-04-20 11:06:31 +08:00
XuChenXu
1cb2be770d 💄 修复显示 2025-04-20 11:05:49 +08:00
XuChenXu
d19d65f75e 🐛 修复响应问题 2025-04-20 11:03:58 +08:00
XuChenXu
a7bdd6033e Merge branch 'master' of https://github.com/ChenXu233/nonebot_plugin_dialectlist 2025-04-20 08:53:33 +08:00
XuChenXu
0d4f69356a 👽 适配chatrecorder 0.7.0 2025-04-20 08:53:28 +08:00
10 changed files with 1194 additions and 1873 deletions

View File

@ -1,369 +1,372 @@
from nonebot import require from nonebot import require
require("nonebot_plugin_chatrecorder") require('nonebot_plugin_chatrecorder')
require("nonebot_plugin_apscheduler") require('nonebot_plugin_apscheduler')
require("nonebot_plugin_htmlrender") require('nonebot_plugin_htmlrender')
require("nonebot_plugin_userinfo") require('nonebot_plugin_userinfo')
require("nonebot_plugin_alconna") require('nonebot_plugin_alconna')
require("nonebot_plugin_uninfo") require('nonebot_plugin_uninfo')
require("nonebot_plugin_cesaa") require('nonebot_plugin_cesaa')
import re import re
import time as t import time as t
import nonebot_plugin_saa as saa
from typing import Union, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Union
import nonebot_plugin_saa as saa
from arclet.alconna import ArparmaBehavior from arclet.alconna import ArparmaBehavior
from arclet.alconna.arparma import Arparma from arclet.alconna.arparma import Arparma
from nonebot import on_command from nonebot import on_command
from nonebot.log import logger
from nonebot.typing import T_State
from nonebot.params import Arg, Depends
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.log import logger
from nonebot.params import Arg, Depends
from nonebot.plugin import PluginMetadata, inherit_supported_adapters from nonebot.plugin import PluginMetadata, inherit_supported_adapters
from nonebot.typing import T_State
from nonebot_plugin_alconna import ( from nonebot_plugin_alconna import (
At, Alconna,
Args, Args,
Field, At,
Match, Field,
Option, Match,
Alconna, Option,
on_alconna, on_alconna,
) )
from nonebot_plugin_chatrecorder import get_message_records from nonebot_plugin_chatrecorder import get_message_records
from nonebot_plugin_session import Session, SessionIdType, extract_session from nonebot_plugin_uninfo import Session, Uninfo, get_session
from nonebot_plugin_uninfo import Uninfo
from .storage import get_cache, build_cache
from .config import Config, plugin_config from .config import Config, plugin_config
from .usage import __usage__ from .storage import build_cache, get_cache
from .time import ( from .time import (
get_datetime_fromisoformat_with_timezone, get_datetime_fromisoformat_with_timezone,
get_datetime_now_with_timezone, get_datetime_now_with_timezone,
parse_datetime, parse_datetime,
) )
from .usage import __usage__
from .utils import ( from .utils import (
got_rank, get_rank_image,
msg_counter, get_user_infos,
get_rank_image, got_rank,
persist_id2user_id, msg_counter,
get_user_infos, persist_id2user_id,
) )
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="B话排行榜", name='B话排行榜',
description="调查群U的B话数量以一定的顺序排序后排序出来。", description='调查群U的B话数量以一定的顺序排序后排序出来。',
usage=__usage__, usage=__usage__,
homepage="https://github.com/ChenXu233/nonebot_plugin_dialectlist", homepage='https://github.com/ChenXu233/nonebot_plugin_dialectlist',
type="application", type='application',
supported_adapters=inherit_supported_adapters( supported_adapters=inherit_supported_adapters(
"nonebot_plugin_chatrecorder", "nonebot_plugin_saa", "nonebot_plugin_alconna" 'nonebot_plugin_chatrecorder',
), 'nonebot_plugin_saa',
config=Config, 'nonebot_plugin_alconna',
),
config=Config,
) )
# 抄的词云不过真的很适合B话榜。 # 抄的词云不过真的很适合B话榜。
class SameTime(ArparmaBehavior): class SameTime(ArparmaBehavior):
def operate(self, interface: Arparma): def operate(self, interface: Arparma):
type = interface.query("type") type = interface.query('type')
time = interface.query("time") time = interface.query('time')
if type is None and time: if type is None and time:
interface.behave_fail() interface.behave_fail()
def wrapper(slot: Union[int, str], content: Optional[str], context) -> str: def wrapper(slot: Union[int, str], content: Optional[str], context) -> str:
if slot == "type" and content: if slot == 'type' and content:
return content return content
return "" # pragma: no cover return '' # pragma: no cover
build_cache_cmd = on_command("build_cache", aliases={"重建缓存"}, block=True) build_cache_cmd = on_command('build_cache', aliases={'重建缓存'}, block=True)
@build_cache_cmd.handle() @build_cache_cmd.handle()
async def _build_cache(bot: Bot, event: Event): async def _build_cache(bot: Bot, event: Event):
await saa.Text("正在重建缓存,请稍等。").send(reply=True) await saa.Text('正在重建缓存,请稍等。').send(reply=True)
await build_cache() await build_cache()
await saa.Text("重建缓存完成。").send(reply=True) await saa.Text('重建缓存完成。').send(reply=True)
b_cmd = on_alconna( b_cmd = on_alconna(
Alconna( Alconna(
"看看B话", '看看B话',
Args["at", [str, At], Field(completion=lambda: "请想要查询的人的QQ号")], Args['at', [str, At], Field(completion=lambda: '请想要查询的人的QQ号')],
Option("-g|--group_id", Args["group_id?", str]), Option('-g|--group_id', Args['group_id?', str]),
Option("-k|--keyword", Args["keyword?", str]), Option('-k|--keyword', Args['keyword?', str]),
), ),
aliases={"kkb"}, aliases={'kkb'},
use_cmd_start=True, use_cmd_start=True,
) )
@b_cmd.handle() @b_cmd.handle()
async def handle_b_cmd( async def handle_b_cmd(
at: Match[str | At], at: Match[str | At],
group_id: Match[str], group_id: Match[str],
keyword: Match[str], keyword: Match[str],
uninfo: Uninfo, uninfo: Uninfo,
session: Session = Depends(extract_session), session: Session = Depends(get_session),
): ):
id = at.result id = at.result
if isinstance(id, At): if isinstance(id, At):
id = id.target id = id.target
if group_id.available: if group_id.available:
gid = group_id.result gid = group_id.result
else: else:
gid = session.id2 gid = session.scene.id
if not gid: if not gid:
await b_cmd.finish("请指定群号。") await b_cmd.finish('请指定群号。')
if keyword.available: if keyword.available:
keywords = keyword.result keywords = keyword.result
else: else:
keywords = None keywords = None
messages = await get_message_records( messages = await get_message_records(
id1s=[id], scene_ids=[gid],
id2s=[gid], user_ids=[id],
id_type=SessionIdType.GROUP, types=['message'], # 排除机器人自己发的消息
include_bot_id=False, exclude_user_ids=plugin_config.excluded_people,
include_bot_type=False, )
types=["message"], # 排除机器人自己发的消息 d = msg_counter(messages, keywords)
exclude_id1s=plugin_config.excluded_people, rank = got_rank(d)
) if not rank:
d = msg_counter(messages, keywords) await b_cmd.finish(
rank = got_rank(d) f'该用户在群“{uninfo.scene.name}”关于“{keyword.result}”的B话数量为0。'
if not rank: )
await b_cmd.finish(
f"该用户在群“{uninfo.scene.name}”关于“{keyword}”的B话数量为0。"
)
await saa.Text( await saa.Text(
f"该用户在群“{uninfo.scene.name}”关于“{keyword}”的B话数量为{rank[0][1]}" f'该用户在群“{uninfo.scene.name}”关于“{keyword.result}”的B话数量为{rank[0][1]}'
).send(reply=True) ).send(reply=True)
rank_cmd = on_alconna( rank_cmd = on_alconna(
Alconna( Alconna(
"B话榜", 'B话榜',
Args["type?", ["今日", "昨日", "本周", "上周", "本月", "上月", "年度", "历史"]][ Args[
"time?", 'type?',
str, ['今日', '昨日', '本周', '上周', '本月', '上月', '年度', '历史'],
], ][
Option("-g|--group_id", Args["group_id?", str]), 'time?',
Option("-k|--keyword", Args["keyword?", str]), str,
behaviors=[SameTime()], ],
), Option('-g|--group_id', Args['group_id?', str]),
aliases={"废话榜"}, Option('-k|--keyword', Args['keyword?', str]),
use_cmd_start=True, behaviors=[SameTime()],
block=True, ),
aliases={'废话榜'},
use_cmd_start=True,
block=True,
) )
rank_cmd.shortcut( rank_cmd.shortcut(
r"(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)B话榜", r'(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)B话榜',
{ {
"prefix": True, 'prefix': True,
"command": "B话榜", 'command': 'B话榜',
"wrapper": wrapper, 'wrapper': wrapper,
"args": ["{type}"], 'args': ['{type}'],
}, },
) )
rank_cmd.shortcut( rank_cmd.shortcut(
r"(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)废话榜", r'(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)废话榜',
{ {
"prefix": True, 'prefix': True,
"command": "废话榜", 'command': '废话榜',
"wrapper": wrapper, 'wrapper': wrapper,
"args": ["{type}"], 'args': ['{type}'],
}, },
) )
# 这段函数完全抄的词云 # 这段函数完全抄的词云
@rank_cmd.handle() @rank_cmd.handle()
async def _group_message( async def _group_message(
state: T_State, state: T_State,
session: Session = Depends(extract_session), session: Session = Depends(get_session),
type: Optional[str] = None, type: Optional[str] = None,
time: Optional[str] = None, time: Optional[str] = None,
group_id: Optional[str] = None, group_id: Optional[str] = None,
keyword: Optional[str] = None, keyword: Optional[str] = None,
): ):
t1 = t.time() t1 = t.time()
state["t1"] = t1 state['t1'] = t1
dt = get_datetime_now_with_timezone() dt = get_datetime_now_with_timezone()
if not group_id: if not group_id:
group_id = session.id2 group_id = session.scene.id
logger.debug(f"session id2: {group_id}") logger.debug(f'session id2: {group_id}')
if group_id: if group_id:
state["group_id"] = group_id state['group_id'] = group_id
state["keyword"] = keyword state['keyword'] = keyword
if not type: if not type:
await rank_cmd.finish(__plugin_meta__.usage) await rank_cmd.finish(__plugin_meta__.usage)
dt = get_datetime_now_with_timezone() dt = get_datetime_now_with_timezone()
if type == "今日": if type == '今日':
state["start"] = dt.replace(hour=0, minute=0, second=0, microsecond=0) state['start'] = dt.replace(hour=0, minute=0, second=0, microsecond=0)
state["stop"] = dt state['stop'] = dt
elif type == "昨日": elif type == '昨日':
state["stop"] = dt.replace(hour=0, minute=0, second=0, microsecond=0) state['stop'] = dt.replace(hour=0, minute=0, second=0, microsecond=0)
state["start"] = state["stop"] - timedelta(days=1) state['start'] = state['stop'] - timedelta(days=1)
elif type == "本周": elif type == '本周':
state["start"] = dt.replace( state['start'] = dt.replace(
hour=0, minute=0, second=0, microsecond=0 hour=0, minute=0, second=0, microsecond=0
) - timedelta(days=dt.weekday()) ) - timedelta(days=dt.weekday())
state["stop"] = dt state['stop'] = dt
elif type == "上周": elif type == '上周':
state["stop"] = dt.replace( state['stop'] = dt.replace(
hour=0, minute=0, second=0, microsecond=0 hour=0, minute=0, second=0, microsecond=0
) - timedelta(days=dt.weekday()) ) - timedelta(days=dt.weekday())
state["start"] = state["stop"] - timedelta(days=7) state['start'] = state['stop'] - timedelta(days=7)
elif type == "本月": elif type == '本月':
state["start"] = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) state['start'] = dt.replace(
state["stop"] = dt day=1, hour=0, minute=0, second=0, microsecond=0
elif type == "上月": )
state["stop"] = dt.replace( state['stop'] = dt
day=1, hour=0, minute=0, second=0, microsecond=0 elif type == '上月':
) - timedelta(microseconds=1) state['stop'] = dt.replace(
state["start"] = state["stop"].replace( day=1, hour=0, minute=0, second=0, microsecond=0
day=1, hour=0, minute=0, second=0, microsecond=0 ) - timedelta(microseconds=1)
) state['start'] = state['stop'].replace(
elif type == "年度": day=1, hour=0, minute=0, second=0, microsecond=0
state["start"] = dt.replace( )
month=1, day=1, hour=0, minute=0, second=0, microsecond=0 elif type == '年度':
) state['start'] = dt.replace(
state["stop"] = dt month=1, day=1, hour=0, minute=0, second=0, microsecond=0
elif type == "历史": )
if time: state['stop'] = dt
plaintext = time elif type == '历史':
if match := re.match(r"^(.+?)(?:~(.+))?$", plaintext): if time:
start = match[1] plaintext = time
stop = match[2] if match := re.match(r'^(.+?)(?:~(.+))?$', plaintext):
try: start = match[1]
state["start"] = get_datetime_fromisoformat_with_timezone(start) stop = match[2]
if stop: try:
state["stop"] = get_datetime_fromisoformat_with_timezone(stop) state['start'] = get_datetime_fromisoformat_with_timezone(
else: start
# 如果没有指定结束日期,则认为是所给日期的当天的词云 )
state["start"] = state["start"].replace( if stop:
hour=0, minute=0, second=0, microsecond=0 state['stop'] = (
) get_datetime_fromisoformat_with_timezone(stop)
state["stop"] = state["start"] + timedelta(days=1) )
except ValueError: else:
await rank_cmd.finish("请输入正确的日期,不然我没法理解呢!") # 如果没有指定结束日期,则认为是所给日期的当天的词云
state['start'] = state['start'].replace(
hour=0, minute=0, second=0, microsecond=0
)
state['stop'] = state['start'] + timedelta(days=1)
except ValueError:
await rank_cmd.finish(
'请输入正确的日期,不然我没法理解呢!'
)
@rank_cmd.got( @rank_cmd.got(
"start", 'start',
prompt="请输入你要查询的起始日期(如 2022-01-01", prompt='请输入你要查询的起始日期(如 2022-01-01',
parameterless=[Depends(parse_datetime("start"))], parameterless=[Depends(parse_datetime('start'))],
) )
@rank_cmd.got( @rank_cmd.got(
"stop", 'stop',
prompt="请输入你要查询的结束日期(如 2022-02-22", prompt='请输入你要查询的结束日期(如 2022-02-22',
parameterless=[Depends(parse_datetime("stop"))], parameterless=[Depends(parse_datetime('stop'))],
) )
@rank_cmd.got("group_id", prompt="请输入你要查询的群号。") @rank_cmd.got('group_id', prompt='请输入你要查询的群号。')
async def handle_rank( async def handle_rank(
state: T_State, state: T_State,
bot: Bot, bot: Bot,
event: Event, event: Event,
session: Session = Depends(extract_session), session: Session = Depends(get_session),
start: datetime = Arg(), start: datetime = Arg(),
stop: datetime = Arg(), stop: datetime = Arg(),
): ):
if id := state['group_id']:
id = str(id)
logger.debug(f'group_id: {id}')
else:
id = session.scene.id
logger.debug(f'group_id: {id}')
if id := state["group_id"]: if not id:
id = str(id) await saa.Text('没有指定群哦').finish()
logger.debug(f"group_id: {id}")
else:
id = session.id2
logger.debug(f"group_id: {id}")
if not id: keyword = state['keyword']
await saa.Text("没有指定群哦").finish()
keyword = state["keyword"] if plugin_config.counting_cache:
if keyword:
await saa.Text('已开启缓存~缓存不支持关键词查询哦').finish()
t1 = t.time()
raw_rank = await get_cache(start, stop, id)
logger.debug(f'获取计数消息花费时间:{t.time() - t1}')
else:
t1 = t.time()
messages = await get_message_records(
scene_ids=[id],
types=['message'], # 排除机器人自己发的消息
time_start=start,
time_stop=stop,
exclude_user_ids=plugin_config.excluded_people,
)
raw_rank = msg_counter(messages, keyword)
logger.debug(f'获取计数消息花费时间:{t.time() - t1}')
if plugin_config.counting_cache: if not raw_rank:
if keyword: await saa.Text(
await saa.Text("已开启缓存~缓存不支持关键词查询哦").finish() '没有获取到排行榜数据哦,请确认时间范围和群号是否正确或者关键词是否存在~'
t1 = t.time() ).finish()
raw_rank = await get_cache(start, stop, id)
logger.debug(f"获取计数消息花费时间:{t.time() - t1}")
else:
t1 = t.time()
messages = await get_message_records(
id2s=[id],
id_type=SessionIdType.GROUP,
include_bot_id=False,
include_bot_type=False,
types=["message"], # 排除机器人自己发的消息
time_start=start,
time_stop=stop,
exclude_id1s=plugin_config.excluded_people,
)
raw_rank = msg_counter(messages, keyword)
logger.debug(f"获取计数消息花费时间:{t.time() - t1}")
if not raw_rank: rank = got_rank(raw_rank)
await saa.Text( ids = await persist_id2user_id([int(i[0]) for i in rank])
"没有获取到排行榜数据哦,请确认时间范围和群号是否正确或者关键词是否存在~" for i in range(len(rank)):
).finish() rank[i][0] = str(ids[i])
logger.debug(rank[i])
rank = got_rank(raw_rank) t1 = t.time()
ids = await persist_id2user_id([int(i[0]) for i in rank]) rank2 = await get_user_infos(bot, event, rank)
for i in range(len(rank)): logger.debug(f'获取用户信息花费时间:{t.time() - t1}')
rank[i][0] = str(ids[i])
logger.debug(rank[i])
t1 = t.time() string: str = ''
rank2 = await get_user_infos(bot, event, rank) if plugin_config.show_text_rank:
logger.debug(f"获取用户信息花费时间:{t.time() - t1}") if keyword:
string += f'关于{keyword}的话痨榜结果:\n'
else:
string += '话痨榜:\n'
string: str = "" for i in rank2:
if plugin_config.show_text_rank: logger.debug(i.user_name)
for i in range(len(rank2)):
str_example = plugin_config.string_format.format(
index=rank2[i].user_index,
nickname=rank2[i].user_nickname,
chatdatanum=rank2[i].user_bnum,
)
string += str_example
if keyword: msg = saa.Text(string)
string += f"关于{keyword}的话痨榜结果:\n"
else:
string += "话痨榜:\n"
for i in rank2: if plugin_config.visualization:
logger.debug(i.user_name) t1 = t.time()
for i in range(len(rank2)): image = await get_rank_image(rank2)
str_example = plugin_config.string_format.format( msg += saa.Image(image)
index=rank2[i].user_index, logger.debug(f'群聊消息渲染图片花费时间:{t.time() - t1}')
nickname=rank2[i].user_nickname,
chatdatanum=rank2[i].user_bnum,
)
string += str_example
msg = saa.Text(string) if plugin_config.suffix:
timecost = t.time() - state['t1']
suffix = saa.Text(plugin_config.string_suffix.format(timecost=timecost))
msg += suffix
if not msg:
await saa.Text('你把可视化都关了哪来的排行榜?').finish()
if plugin_config.visualization: if plugin_config.aggregate_transmission:
t1 = t.time() await saa.AggregatedMessageFactory([msg]).finish()
image = await get_rank_image(rank2) else:
msg += saa.Image(image) await msg.finish(reply=True)
logger.debug(f"群聊消息渲染图片花费时间:{t.time() - t1}")
if plugin_config.suffix:
timecost = t.time() - state["t1"]
suffix = saa.Text(plugin_config.string_suffix.format(timecost=timecost))
msg += suffix
if not msg:
await saa.Text("你把可视化都关了哪来的排行榜?").finish()
if plugin_config.aggregate_transmission:
await saa.AggregatedMessageFactory([msg]).finish()
else:
await msg.finish(reply=True)

View File

@ -1,6 +1,7 @@
from pydantic import BaseModel from typing import List, Optional
from typing import Optional, List
from nonebot import get_driver, get_plugin_config from nonebot import get_driver, get_plugin_config
from pydantic import BaseModel
class ScopedConfig(BaseModel): class ScopedConfig(BaseModel):

View File

@ -1,61 +0,0 @@
"""empty message
迁移 ID: 873ad3421460
父迁移:
创建时间: 2025-03-29 14:30:11.910461
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "873ad3421460"
down_revision: str | Sequence[str] | None = None
branch_labels: str | Sequence[str] | None = ("nonebot_plugin_dialectlist",)
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = "") -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"nonebot_plugin_dialectlist_messagecountcache",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("time", sa.DateTime(), nullable=False),
sa.Column("session_id", sa.Integer(), nullable=False),
sa.Column("session_bnum", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint(
"id", name=op.f("pk_nonebot_plugin_dialectlist_messagecountcache")
),
info={"bind_key": "nonebot_plugin_dialectlist"},
)
with op.batch_alter_table(
"nonebot_plugin_dialectlist_messagecountcache", schema=None
) as batch_op:
batch_op.create_index(
batch_op.f("ix_nonebot_plugin_dialectlist_messagecountcache_session_id"),
["session_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade(name: str = "") -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table(
"nonebot_plugin_dialectlist_messagecountcache", schema=None
) as batch_op:
batch_op.drop_index(
batch_op.f("ix_nonebot_plugin_dialectlist_messagecountcache_session_id")
)
op.drop_table("nonebot_plugin_dialectlist_messagecountcache")
# ### end Alembic commands ###

View File

@ -0,0 +1,47 @@
"""update dialectlist
迁移 ID: fb88e4d27eb8
父迁移: 60daff81fcdc
创建时间: 2025-04-20 11:00:50.931679
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
revision: str = 'fb88e4d27eb8'
down_revision: str | Sequence[str] | None = '60daff81fcdc'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade(name: str = "") -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('sessionmodel',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bot_id', sa.String(length=64), nullable=False),
sa.Column('bot_type', sa.String(length=32), nullable=False),
sa.Column('platform', sa.String(length=32), nullable=False),
sa.Column('level', sa.Integer(), nullable=False),
sa.Column('id1', sa.String(length=64), nullable=False),
sa.Column('id2', sa.String(length=64), nullable=False),
sa.Column('id3', sa.String(length=64), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_sessionmodel')),
sa.UniqueConstraint('bot_id', 'bot_type', 'platform', 'level', 'id1', 'id2', 'id3', name='unique_session'),
info={'bind_key': ''}
)
# ### end Alembic commands ###
def downgrade(name: str = "") -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('sessionmodel')
# ### end Alembic commands ###

View File

@ -1,8 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Union from typing import Union
from sqlalchemy import Integer
from nonebot_plugin_orm import Model from nonebot_plugin_orm import Model
from nonebot_plugin_userinfo import UserInfo from nonebot_plugin_userinfo import UserInfo
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column

View File

@ -1,23 +1,22 @@
import os
import json import json
import os
from datetime import datetime from datetime import datetime
from sqlalchemy import delete, or_, select
from nonebot import get_driver from nonebot import get_driver
from nonebot.adapters import Bot, Event
from nonebot.log import logger from nonebot.log import logger
from nonebot.params import Depends
from nonebot.adapters import Event, Bot
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
from nonebot.params import Depends
from .model import MessageCountCache
from .config import plugin_config
from nonebot_plugin_localstore import get_data_file
from nonebot_plugin_chatrecorder import get_message_records from nonebot_plugin_chatrecorder import get_message_records
from nonebot_plugin_chatrecorder.utils import remove_timezone from nonebot_plugin_chatrecorder.utils import remove_timezone
from nonebot_plugin_session import extract_session, Session from nonebot_plugin_localstore import get_data_file
from nonebot_plugin_session_orm import SessionModel, get_session_persist_id
from nonebot_plugin_orm import get_session from nonebot_plugin_orm import get_session
from nonebot_plugin_session import Session, extract_session
from nonebot_plugin_session_orm import SessionModel, get_session_persist_id
from sqlalchemy import delete, or_, select
from .config import plugin_config
from .model import MessageCountCache
async def get_cache(time_start: datetime, time_stop: datetime, group_id: str): async def get_cache(time_start: datetime, time_stop: datetime, group_id: str):

View File

@ -1,13 +1,12 @@
from zoneinfo import ZoneInfo
from typing import Optional, Union
from datetime import datetime, time, tzinfo from datetime import datetime, time, tzinfo
from typing import Optional, Union
from zoneinfo import ZoneInfo
from nonebot.adapters import Message
from nonebot.params import Arg from nonebot.params import Arg
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.adapters import Message
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_alconna import AlconnaMatcher from nonebot_plugin_alconna import AlconnaMatcher
from nonebot_plugin_apscheduler import scheduler
from .config import plugin_config from .config import plugin_config

View File

@ -1,279 +1,263 @@
import asyncio
import os import os
import re import re
import httpx
import asyncio
import unicodedata import unicodedata
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import or_, select
from sqlalchemy.sql import ColumnElement
from nonebot.log import logger import httpx
from nonebot.params import Depends
from nonebot.compat import model_dump
from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.compat import model_dump
from nonebot_plugin_orm import get_session from nonebot.log import logger
from nonebot_plugin_session import Session, SessionLevel, extract_session from nonebot.matcher import Matcher
from nonebot_plugin_userinfo import get_user_info, UserInfo from nonebot.params import Depends
from nonebot_plugin_userinfo.exception import NetworkError
from nonebot_plugin_localstore import get_cache_dir
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_session_orm import SessionModel
from nonebot_plugin_chatrecorder import MessageRecord from nonebot_plugin_chatrecorder import MessageRecord
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_localstore import get_cache_dir
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo, get_user_info
from nonebot_plugin_uninfo import Session
from nonebot_plugin_uninfo.model import SceneType
from nonebot_plugin_uninfo.orm import SessionModel, UserModel
from nonebot_plugin_uninfo import get_session as extract_session
from nonebot_plugin_userinfo.exception import NetworkError
from sqlalchemy import or_, select
from .model import UserRankInfo
from .config import plugin_config from .config import plugin_config
from .model import UserRankInfo
cache_path = get_cache_dir("nonebot_plugin_dialectlist") cache_path = get_cache_dir('nonebot_plugin_dialectlist')
async def ensure_group(matcher: Matcher, session: Session = Depends(extract_session)): async def ensure_group(
"""确保在群组中使用""" matcher: Matcher, session: Session = Depends(extract_session)
if session.level not in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: ):
await matcher.finish("请在群组中使用!") """确保在群组中使用"""
if session.scene.type not in [SceneType.GROUP, SceneType.GUILD]:
await matcher.finish('请在群组中使用!')
async def persist_id2user_id(ids: List) -> List[str]: async def persist_id2user_id(ids: List) -> List[str]:
user_ids = [] user_ids = []
async with get_session() as db_session: user_persist_ids = []
for i in ids: async with get_session() as db_session:
user_id = (await db_session.scalar(select(SessionModel).where(or_(*[SessionModel.id == i])))).id1 # type: ignore for i in ids:
user_ids.append(user_id) session = await db_session.scalar(
return user_ids select(SessionModel).where(or_(*[SessionModel.id == i]))
)
if session is not None:
user_persist_id = session.user_persist_id
user_persist_ids.append(user_persist_id)
for i in user_persist_ids:
user = await db_session.scalar(
select(UserModel).where(UserModel.id == i)
)
if user is not None:
user_ids.append(user.user_id)
return user_ids
async def user_id2persist_id(ids: List[str]) -> List[int]:
whereclause: List[ColumnElement[bool]] = []
whereclause.append(or_(*[SessionModel.id1 == id for id in ids]))
statement = (
select(SessionModel).where(*whereclause)
# .join(SessionModel, SessionModel.id == MessageRecord.session_persist_id)
)
async with get_session() as db_session:
records = (await db_session.scalars(statement)).all()
return [i.id for i in records]
async def group_id2persist_id(ids: List[str]) -> List[int]:
persist_ids = []
async with get_session() as db_session:
for i in ids:
persist_id = (await db_session.scalar(select(SessionModel).where(or_(*[SessionModel.id2 == i])))).id # type: ignore
persist_ids.append(persist_id)
return persist_ids
async def persist_id2group_id(ids: List[str]) -> List[str]:
whereclause: List[ColumnElement[bool]] = []
whereclause.append(or_(*[SessionModel.id == id for id in ids]))
statement = (
select(SessionModel).where(*whereclause)
# .join(SessionModel, SessionModel.id == MessageRecord.session_persist_id)
)
async with get_session() as db_session:
records = (await db_session.scalars(statement)).all()
return [i.id2 for i in records]
def msg_counter( def msg_counter(
msg_list: List[MessageRecord], keyword: Optional[str] msg_list: List[MessageRecord], keyword: Optional[str]
) -> Dict[str, int]: ) -> Dict[str, int]:
"""### 计算每个人的消息量 """### 计算每个人的消息量
Args: Args:
msg_list (list[MessageRecord]): 需要处理的消息列表 msg_list (list[MessageRecord]): 需要处理的消息列表
Returns: Returns:
(dict[str,int]): 处理后的消息数量字典,键为用户,值为消息数量 (dict[str,int]): 处理后的消息数量字典,键为用户,值为消息数量
""" """
lst: Dict[str, int] = {} lst: Dict[str, int] = {}
msg_len = len(msg_list) msg_len = len(msg_list)
logger.info("wow , there are {} msgs to count !!!".format(msg_len)) logger.info('wow , there are {} msgs to count !!!'.format(msg_len))
for i in msg_list: for i in msg_list:
# logger.debug(f"processing msg {i.plain_text}") # logger.debug(f"processing msg {i.plain_text}")
if keyword: if keyword:
match = re.search(keyword, i.plain_text) match = re.search(keyword, i.plain_text)
if not match: if not match:
continue continue
try: try:
lst[str(i.session_persist_id)] += 1 lst[str(i.session_persist_id)] += 1
except KeyError: except KeyError:
lst[str(i.session_persist_id)] = 1 lst[str(i.session_persist_id)] = 1
logger.debug(f"finish counting, result is {lst}") logger.debug(f'finish counting, result is {lst}')
return lst return lst
def got_rank(msg_dict: Dict[str, int]) -> List: def got_rank(msg_dict: Dict[str, int]) -> List:
"""### 获得排行榜 """### 获得排行榜
Args: Args:
msg_dict (Dict[str,int]): 要处理的字典 msg_dict (Dict[str,int]): 要处理的字典
Returns: Returns:
List[Tuple[str,int]]: 排行榜列表(已排序) List[Tuple[str,int]]: 排行榜列表(已排序)
""" """
rank = [] rank = []
while len(rank) < plugin_config.get_num: while len(rank) < plugin_config.get_num:
try: try:
max_key = max(msg_dict.items(), key=lambda x: x[1]) max_key = max(msg_dict.items(), key=lambda x: x[1])
rank.append(list(max_key)) rank.append(list(max_key))
msg_dict.pop(max_key[0]) msg_dict.pop(max_key[0])
except ValueError: except ValueError:
logger.error( logger.error(
"群内拥有聊天记录的人数不足,无法获取到长度为{}的排行榜,已将长度变化为:{}".format( '群内拥有聊天记录的人数不足,无法获取到长度为{}的排行榜,已将长度变化为:{}'.format(
plugin_config.get_num, len(rank) plugin_config.get_num, len(rank)
) )
) )
break break
return rank return rank
def remove_control_characters(string: str) -> str: def remove_control_characters(string: str) -> str:
"""### 将字符串中的控制符去除 """### 将字符串中的控制符去除
Args: Args:
string (str): 需要去除的字符串 string (str): 需要去除的字符串
Returns: Returns:
(str): 经过处理的字符串 (str): 经过处理的字符串
""" """
return "".join(ch for ch in string if unicodedata.category(ch)[0] != "C") return ''.join(ch for ch in string if unicodedata.category(ch)[0] != 'C')
async def get_rank_image(rank: List[UserRankInfo]) -> bytes: async def get_rank_image(rank: List[UserRankInfo]) -> bytes:
for i in rank: for i in rank:
if i.user_avatar: if i.user_avatar:
try: try:
user_avatar = i.user_avatar_bytes user_avatar = i.user_avatar_bytes
except NotImplementedError: except NotImplementedError:
user_avatar = open( user_avatar = open(
os.path.dirname(os.path.abspath(__file__)) os.path.dirname(os.path.abspath(__file__))
+ "/template/avatar/default.jpg", + '/template/avatar/default.jpg',
"rb", 'rb',
).read() ).read()
# if not os.path.exists(cache_path / str(i.user_id)): # if not os.path.exists(cache_path / str(i.user_id)):
with open(cache_path / (str(i.user_id) + ".jpg"), "wb") as f: with open(cache_path / (str(i.user_id) + '.jpg'), 'wb') as f:
f.write(user_avatar) f.write(user_avatar)
if plugin_config.template_path[:2] == "./": if plugin_config.template_path[:2] == './':
path = ( path = (
os.path.dirname(os.path.abspath(__file__)) + plugin_config.template_path[1:] os.path.dirname(os.path.abspath(__file__))
) + plugin_config.template_path[1:]
else: )
path = plugin_config.template_path else:
path = plugin_config.template_path
path_dir, filename = os.path.split(path) path_dir, filename = os.path.split(path)
logger.debug( logger.debug(
os.path.dirname(os.path.abspath(__file__)) + plugin_config.template_path[1:] os.path.dirname(os.path.abspath(__file__))
) + plugin_config.template_path[1:]
return await template_to_pic( )
path_dir, return await template_to_pic(
filename, path_dir,
{ filename,
"users": rank, {
"cache_path": cache_path, 'users': rank,
"file_path": os.path.dirname(os.path.abspath(__file__)), 'cache_path': cache_path,
}, 'file_path': os.path.dirname(os.path.abspath(__file__)),
pages={"viewport": {"width": 1000, "height": 10}}, },
) pages={'viewport': {'width': 1000, 'height': 10}},
)
def _get_user_nickname(user_info: UserInfo) -> str: def _get_user_nickname(user_info: UserInfo) -> str:
user_nickname = ( user_nickname = (
user_info.user_displayname user_info.user_displayname
if user_info.user_displayname if user_info.user_displayname
else user_info.user_name if user_info.user_name else user_info.user_id else user_info.user_name
) if user_info.user_name
return user_nickname else user_info.user_id
)
return user_nickname
async def _get_user_default_avatar() -> bytes: async def _get_user_default_avatar() -> bytes:
img = open( img = open(
os.path.dirname(os.path.abspath(__file__)) + "/template/avatar/default.jpg", os.path.dirname(os.path.abspath(__file__))
"rb", + '/template/avatar/default.jpg',
).read() 'rb',
return img ).read()
return img
async def _get_user_avatar(user: UserInfo, client: httpx.AsyncClient) -> bytes: async def _get_user_avatar(user: UserInfo, client: httpx.AsyncClient) -> bytes:
if not user.user_avatar: if not user.user_avatar:
return await _get_user_default_avatar() return await _get_user_default_avatar()
url = user.user_avatar.get_url() url = user.user_avatar.get_url()
for i in range(3): for i in range(3):
try: try:
resp = await client.get(url, timeout=10) resp = await client.get(url, timeout=10)
resp.raise_for_status() resp.raise_for_status()
return resp.content return resp.content
except Exception as e: except Exception as e:
logger.warning(f"Error downloading {url}, retry {i}/3: {e}") logger.warning(f'Error downloading {url}, retry {i}/3: {e}')
await asyncio.sleep(3) await asyncio.sleep(3)
raise NetworkError(f"{url} 下载失败!") raise NetworkError(f'{url} 下载失败!')
def get_default_user_info() -> UserInfo: def get_default_user_info() -> UserInfo:
user_info = UserInfo( user_info = UserInfo(
user_id="114514", user_id='114514',
user_name="鬼知道这谁bot获取不了", user_name='鬼知道这谁bot获取不了',
) )
return user_info return user_info
async def get_user_infos( async def get_user_infos(
bot: Bot, bot: Bot,
event: Event, event: Event,
rank: List, rank: List,
use_cache: bool = plugin_config.use_user_info_cache, use_cache: bool = plugin_config.use_user_info_cache,
) -> List[UserRankInfo]: ) -> List[UserRankInfo]:
user_ids = [i[0] for i in rank]
pool = [get_user_info(bot, event, id, use_cache) for id in user_ids]
user_infos = await asyncio.gather(*pool)
user_ids = [i[0] for i in rank] async with httpx.AsyncClient() as client:
pool = [get_user_info(bot, event, id, use_cache) for id in user_ids] pool = []
user_infos = await asyncio.gather(*pool) for i in user_infos:
if not i:
pool.append(_get_user_default_avatar())
continue
if i.user_avatar:
pool.append(_get_user_avatar(i, client))
user_avatars = await asyncio.gather(*pool)
async with httpx.AsyncClient() as client: for i in user_avatars:
pool = [] if not i:
for i in user_infos: user_avatars[
if not i: user_avatars.index(i)
pool.append(_get_user_default_avatar()) ] = await _get_user_default_avatar()
continue
if i.user_avatar:
pool.append(_get_user_avatar(i, client))
user_avatars = await asyncio.gather(*pool)
for i in user_avatars: total = sum([i[1] for i in rank])
if not i: rank2 = []
user_avatars[user_avatars.index(i)] = await _get_user_default_avatar() for i in range(len(rank)):
user_info = user_infos[i]
if not user_info:
user_info = get_default_user_info()
total = sum([i[1] for i in rank]) user = UserRankInfo(
rank2 = [] **model_dump(user_info),
for i in range(len(rank)): user_bnum=rank[i][1],
user_proportion=round(rank[i][1] / total * 100, 2),
user_index=i + 1,
user_nickname=_get_user_nickname(user_info),
user_avatar_bytes=user_avatars[i],
)
print(user.user_gender)
if user.user_gender == 'male':
user.user_gender = ''
elif user.user_gender == 'female':
user.user_gender = ''
else:
user.user_gender = '🤔'
rank2.append(user)
user_info = user_infos[i] return rank2
if not user_info:
user_info = get_default_user_info()
user = UserRankInfo(
**model_dump(user_info),
user_bnum=rank[i][1],
user_proportion=round(rank[i][1] / total * 100, 2),
user_index=i + 1,
user_nickname=_get_user_nickname(user_info),
user_avatar_bytes=user_avatars[i],
)
print(user.user_gender)
if user.user_gender == "male":
user.user_gender = ""
elif user.user_gender == "female":
user.user_gender = ""
else:
user.user_gender = "🤔"
rank2.append(user)
return rank2

1930
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
[project] [project]
name = "nonebot-plugin-dialectlist" name = "nonebot-plugin-dialectlist"
version = "2.6.2" version = "2.7.0"
description = "看看你群群友有多能说" description = "看看你群群友有多能说"
authors = [ authors = [{ name = "Chen_Xu233", email = "woyerpa@outlook.com" }]
{name = "Chen_Xu233", email = "woyerpa@outlook.com"},
]
dependencies = [ dependencies = [
"nonebot-plugin-chatrecorder>=0.6.0,<0.7.0", "nonebot-plugin-chatrecorder>=0.7.0",
"nonebot-plugin-orm[default]", "nonebot-plugin-orm[default]",
"nonebot-plugin-apscheduler>=0.4.0", "nonebot-plugin-apscheduler>=0.4.0",
"nonebot-plugin-alconna>=0.50.2", "nonebot-plugin-alconna>=0.50.2",
@ -19,7 +17,7 @@ dependencies = [
] ]
requires-python = ">=3.9,<4.0" requires-python = ">=3.9,<4.0"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = { text = "MIT" }
[project.optional-dependencies] [project.optional-dependencies]
@ -30,9 +28,7 @@ dev = [
"nb-cli>=0.7.6", "nb-cli>=0.7.6",
"py-spy>=0.3.14", "py-spy>=0.3.14",
] ]
Test = [ Test = ["nonebot-adapter-onebot>=2.4.4"]
"nonebot-adapter-onebot>=2.4.4",
]
[tool.pdm] [tool.pdm]
distribution = true distribution = true