forked from bot/app
✨ 智障回复功能
This commit is contained in:
parent
be28116a98
commit
206651da94
@ -1,6 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any, AnyStr
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
import pip
|
import pip
|
||||||
@ -211,7 +211,11 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
function_name = result.main_args.get("function")
|
function_name = result.main_args.get("function")
|
||||||
args: tuple[str] = result.main_args.get("args", ())
|
args: tuple[str] = result.main_args.get("args", ())
|
||||||
_args = []
|
_args = []
|
||||||
_kwargs = {}
|
_kwargs = {
|
||||||
|
"USER_ID" : str(event.user_id),
|
||||||
|
"GROUP_ID": str(event.group_id) if event.message_type == "group" else "0",
|
||||||
|
"BOT_ID" : str(bot.self_id)
|
||||||
|
}
|
||||||
|
|
||||||
for arg in args:
|
for arg in args:
|
||||||
arg = arg.replace("\\=", "EQUAL_SIGN")
|
arg = arg.replace("\\=", "EQUAL_SIGN")
|
||||||
@ -227,7 +231,7 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
_args.append(arg.replace("EQUAL_SIGN", "="))
|
_args.append(arg.replace("EQUAL_SIGN", "="))
|
||||||
|
|
||||||
ly_func = get_function(function_name)
|
ly_func = get_function(function_name)
|
||||||
ly_func.bot = bot if "bot_id" not in _kwargs else nonebot.get_bot(_kwargs["bot_id"])
|
ly_func.bot = bot if "BOT_ID" not in _kwargs else nonebot.get_bot(_kwargs["BOT_ID"])
|
||||||
ly_func.matcher = matcher
|
ly_func.matcher = matcher
|
||||||
|
|
||||||
await ly_func(*tuple(_args), **_kwargs)
|
await ly_func(*tuple(_args), **_kwargs)
|
||||||
@ -236,7 +240,7 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
@on_alconna(
|
@on_alconna(
|
||||||
command=Alconna(
|
command=Alconna(
|
||||||
"/api",
|
"/api",
|
||||||
Args["api", str]["args", MultiVar(str), ()],
|
Args["api", str]["args", MultiVar(AnyStr), ()],
|
||||||
),
|
),
|
||||||
permission=SUPERUSER
|
permission=SUPERUSER
|
||||||
).handle()
|
).handle()
|
||||||
@ -253,10 +257,12 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
"""
|
"""
|
||||||
api_name = result.main_args.get("api")
|
api_name = result.main_args.get("api")
|
||||||
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数,但每个参数间用空格分隔,空格是%20
|
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数,但每个参数间用空格分隔,空格是%20
|
||||||
|
print(args)
|
||||||
args_dict = {}
|
args_dict = {}
|
||||||
|
|
||||||
for arg in args:
|
for arg in args:
|
||||||
key, value = arg.split("=", 1)
|
key, value = arg.split("=", 1)
|
||||||
|
|
||||||
args_dict[key] = unescape(value.replace("%20", " "))
|
args_dict[key] = unescape(value.replace("%20", " "))
|
||||||
|
|
||||||
if api_name in need_user_id and "user_id" not in args_dict:
|
if api_name in need_user_id and "user_id" not in args_dict:
|
||||||
@ -265,7 +271,10 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
args_dict["group_id"] = str(event.group_id)
|
args_dict["group_id"] = str(event.group_id)
|
||||||
|
|
||||||
if "message" in args_dict:
|
if "message" in args_dict:
|
||||||
args_dict["message"] = Message(args_dict["message"])
|
args_dict["message"] = Message(eval(args_dict["message"]))
|
||||||
|
|
||||||
|
if "messages" in args_dict:
|
||||||
|
args_dict["messages"] = Message(eval(args_dict["messages"]))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await bot.call_api(api_name, **args_dict)
|
result = await bot.call_api(api_name, **args_dict)
|
||||||
|
18
liteyuki/plugins/liteyuki_smart_reply/__init__.py
Normal file
18
liteyuki/plugins/liteyuki_smart_reply/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from .monitors import *
|
||||||
|
from .matchers import *
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = "snowykami"
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="轻雪智障回复",
|
||||||
|
description="",
|
||||||
|
usage="",
|
||||||
|
type="application",
|
||||||
|
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||||
|
extra={
|
||||||
|
"liteyuki": True,
|
||||||
|
"toggleable" : True,
|
||||||
|
"default_enable" : True,
|
||||||
|
}
|
||||||
|
)
|
85
liteyuki/plugins/liteyuki_smart_reply/matchers.py
Normal file
85
liteyuki/plugins/liteyuki_smart_reply/matchers.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
|
||||||
|
from nonebot import Bot, on_message, get_driver, require
|
||||||
|
from nonebot.internal.matcher import Matcher
|
||||||
|
from nonebot.permission import SUPERUSER
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
|
||||||
|
from liteyuki.utils.base.ly_typing import T_MessageEvent
|
||||||
|
from .utils import get_keywords
|
||||||
|
from liteyuki.utils.base.word_bank import get_reply
|
||||||
|
from liteyuki.utils.event import get_message_type
|
||||||
|
from liteyuki.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
|
||||||
|
from liteyuki.utils.base.data_manager import group_db, Group
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna")
|
||||||
|
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma
|
||||||
|
|
||||||
|
nicknames = set()
|
||||||
|
driver = get_driver()
|
||||||
|
group_reply_probability: dict[str, float] = {
|
||||||
|
}
|
||||||
|
default_reply_probability = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
@on_alconna(
|
||||||
|
Alconna(
|
||||||
|
"set-reply-probability",
|
||||||
|
Args["probability", float, default_reply_probability],
|
||||||
|
),
|
||||||
|
aliases={"设置回复概率"},
|
||||||
|
permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER,
|
||||||
|
).handle()
|
||||||
|
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
|
||||||
|
# 修改内存和数据库的概率值
|
||||||
|
if get_message_type(event) == "group":
|
||||||
|
group_id = event.group_id
|
||||||
|
probability = result.main_args.get("probability")
|
||||||
|
# 保存到内存
|
||||||
|
group_reply_probability[group_id] = probability
|
||||||
|
# 保存到数据库
|
||||||
|
group: Group = group_db.where_one(Group(), "group_id = ?", group_id, default=Group(group_id=str(group_id)))
|
||||||
|
group.config["reply_probability"] = probability
|
||||||
|
group_db.save(group)
|
||||||
|
|
||||||
|
await matcher.send(f"已将群组{group_id}的回复概率设置为{probability}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@driver.on_bot_connect
|
||||||
|
async def _(bot: Bot):
|
||||||
|
global nicknames
|
||||||
|
nicknames.update(bot.config.nickname)
|
||||||
|
# 从数据库加载群组的回复概率
|
||||||
|
groups = group_db.where_all(Group(), default=[])
|
||||||
|
for group in groups:
|
||||||
|
group_reply_probability[group.group_id] = group.config.get("reply_probability", default_reply_probability)
|
||||||
|
|
||||||
|
|
||||||
|
@on_message(priority=100).handle()
|
||||||
|
async def _(event: T_MessageEvent, bot: Bot, state: T_State, matcher: Matcher):
|
||||||
|
kws = await get_keywords(event.message.extract_plain_text())
|
||||||
|
|
||||||
|
tome = False
|
||||||
|
if await to_me()(event=event, bot=bot, state=state):
|
||||||
|
tome = True
|
||||||
|
else:
|
||||||
|
for kw in kws:
|
||||||
|
if kw in nicknames:
|
||||||
|
tome = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# 回复概率
|
||||||
|
message_type = get_message_type(event)
|
||||||
|
if tome or message_type == "private":
|
||||||
|
p = 1.0
|
||||||
|
else:
|
||||||
|
p = group_reply_probability.get(event.group_id, default_reply_probability)
|
||||||
|
|
||||||
|
if random.random() < p:
|
||||||
|
await asyncio.sleep(random.random() * 2)
|
||||||
|
if reply := get_reply(kws):
|
||||||
|
await matcher.send(reply)
|
||||||
|
return
|
0
liteyuki/plugins/liteyuki_smart_reply/monitors.py
Normal file
0
liteyuki/plugins/liteyuki_smart_reply/monitors.py
Normal file
13
liteyuki/plugins/liteyuki_smart_reply/utils.py
Normal file
13
liteyuki/plugins/liteyuki_smart_reply/utils.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from jieba import lcut
|
||||||
|
from nonebot.utils import run_sync
|
||||||
|
|
||||||
|
|
||||||
|
@run_sync
|
||||||
|
def get_keywords(text: str) -> list[str, ...]:
|
||||||
|
"""
|
||||||
|
获取关键词
|
||||||
|
Args:
|
||||||
|
text: 文本
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
return lcut(text)
|
3
liteyuki/resources/liteyuki_words/metadata.yml
Normal file
3
liteyuki/resources/liteyuki_words/metadata.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
name: 轻雪词库
|
||||||
|
description: For Liteyuki Auto Reply
|
||||||
|
version: 2024.4.26
|
19
liteyuki/resources/liteyuki_words/word_bank/LICENSE
Normal file
19
liteyuki/resources/liteyuki_words/word_bank/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
Copyright (c) 2020 NoneBot Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
31
liteyuki/resources/liteyuki_words/word_bank/README.md
Normal file
31
liteyuki/resources/liteyuki_words/word_bank/README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 这是什么 | What's this
|
||||||
|
|
||||||
|
一个特~~二刺螈~~(文爱)的适用于任何bot的词库<br>
|
||||||
|
好像有点涩?(不止一点
|
||||||
|
|
||||||
|
## 一些使用建议 | Using advices
|
||||||
|
|
||||||
|
- 词库返回中的“你”,可替换成目标昵称。同理,“我”也可替换成bot的昵称
|
||||||
|
- 如需使用分词,建议匹配无结果时将本词库key打乱,放回原句遍历判断
|
||||||
|
|
||||||
|
## 相关项目 | Related projects
|
||||||
|
|
||||||
|
> 欢迎提交pr以明示使用本词库的项目~
|
||||||
|
|
||||||
|
- [MoeChat](https://github.com/Fzoss/MoeChat) 聊天UI
|
||||||
|
- [ATRI](https://github.com/Kyomotoi/ATRI) 高性能萝卜子!
|
||||||
|
- [绪山真寻Bot](https://github.com/HibiKier/zhenxun_bot) 非常可爱的绪山真寻bot
|
||||||
|
- [ZeroBot-Plugin](https://github.com/FloatTech/ZeroBot-Plugin) 基于 ZeroBot 的 OneBot 插件
|
||||||
|
- [Liteyuki Bot](https://github.com/snowyfirefly/Liteyuki-Bot) 非常可爱的,有独立思维(雾)的轻雪Bot
|
||||||
|
- [kmua bot](https://github.com/krau/kmua-bot) Telegram 上的可爱文爱 bot
|
||||||
|
|
||||||
|
|
||||||
|
## 贡献 | Contribute
|
||||||
|
|
||||||
|
只要你的词和回复够味,都可以向此库提交!
|
||||||
|
|
||||||
|
## 许可 | License
|
||||||
|
|
||||||
|
本项目使用 MIT
|
||||||
|
|
||||||
|
![](https://cdn.jsdelivr.net/gh/Kyomotoi/CDN@master/noting/88674944_p0.png)
|
1924
liteyuki/resources/liteyuki_words/word_bank/data.json
Normal file
1924
liteyuki/resources/liteyuki_words/word_bank/data.json
Normal file
File diff suppressed because it is too large
Load Diff
15410
liteyuki/resources/liteyuki_words/word_bank/傲娇系二次元bot词库5千词V1.2.json
Normal file
15410
liteyuki/resources/liteyuki_words/word_bank/傲娇系二次元bot词库5千词V1.2.json
Normal file
File diff suppressed because it is too large
Load Diff
39407
liteyuki/resources/liteyuki_words/word_bank/可爱系二次元bot词库1.5万词V1.2.json
Normal file
39407
liteyuki/resources/liteyuki_words/word_bank/可爱系二次元bot词库1.5万词V1.2.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -34,6 +34,7 @@ class Group(LiteModel):
|
|||||||
enabled_plugins: list[str] = Field([], alias="enabled_plugins")
|
enabled_plugins: list[str] = Field([], alias="enabled_plugins")
|
||||||
disabled_plugins: list[str] = Field([], alias="disabled_plugins")
|
disabled_plugins: list[str] = Field([], alias="disabled_plugins")
|
||||||
enable: bool = Field(True, alias="enable") # 群聊全局机器人是否启用
|
enable: bool = Field(True, alias="enable") # 群聊全局机器人是否启用
|
||||||
|
config: dict = Field({}, alias="config")
|
||||||
|
|
||||||
|
|
||||||
class InstalledPlugin(LiteModel):
|
class InstalledPlugin(LiteModel):
|
||||||
|
@ -64,9 +64,23 @@ class LiteyukiFunction:
|
|||||||
line: 行数
|
line: 行数
|
||||||
Returns:
|
Returns:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "${" in cmd:
|
||||||
|
# 此种情况下,{}内容不用管,只对${}内的内容进行format
|
||||||
|
for i in range(len(cmd) - 1):
|
||||||
|
if cmd[i] == "$" and cmd[i + 1] == "{":
|
||||||
|
end = cmd.find("}", i)
|
||||||
|
key = cmd[i + 2:end]
|
||||||
|
cmd = cmd.replace(f"${{{key}}}", str(self.kwargs_data.get(key, "")))
|
||||||
|
else:
|
||||||
|
cmd = cmd.format(*self.args_data, **self.kwargs_data)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
no_head = cmd.split(" ", 1)[1] if len(cmd.split(" ")) > 1 else ""
|
no_head = cmd.split(" ", 1)[1] if len(cmd.split(" ")) > 1 else ""
|
||||||
try:
|
try:
|
||||||
head, args, kwargs = self.get_args(cmd)
|
head, cmd_args, cmd_kwargs = self.get_args(cmd)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Parsing error in {self.name} at line {line}: {e}"
|
error_msg = f"Parsing error in {self.name} at line {line}: {e}"
|
||||||
nonebot.logger.error(error_msg)
|
nonebot.logger.error(error_msg)
|
||||||
@ -75,7 +89,7 @@ class LiteyukiFunction:
|
|||||||
|
|
||||||
if head == "var":
|
if head == "var":
|
||||||
# 变量定义
|
# 变量定义
|
||||||
self.kwargs_data.update(kwargs)
|
self.kwargs_data.update(cmd_kwargs)
|
||||||
|
|
||||||
elif head == "cmd":
|
elif head == "cmd":
|
||||||
# 在当前计算机上执行命令
|
# 在当前计算机上执行命令
|
||||||
@ -83,18 +97,18 @@ class LiteyukiFunction:
|
|||||||
|
|
||||||
elif head == "api":
|
elif head == "api":
|
||||||
# 调用Bot API 需要Bot实例
|
# 调用Bot API 需要Bot实例
|
||||||
await self.bot.call_api(args[1], **kwargs)
|
await self.bot.call_api(cmd_args[1], **cmd_kwargs)
|
||||||
|
|
||||||
elif head == "function":
|
elif head == "function":
|
||||||
# 调用轻雪函数
|
# 调用轻雪函数
|
||||||
func = get_function(args[1])
|
func = get_function(cmd_args[1])
|
||||||
func.bot = self.bot
|
func.bot = self.bot
|
||||||
func.matcher = self.matcher
|
func.matcher = self.matcher
|
||||||
await func(*args[2:], **kwargs)
|
await func(*cmd_args[2:], **cmd_kwargs)
|
||||||
|
|
||||||
elif head == "sleep":
|
elif head == "sleep":
|
||||||
# 等待一段时间
|
# 等待一段时间
|
||||||
await asyncio.sleep(float(args[1]))
|
await asyncio.sleep(float(cmd_args[1]))
|
||||||
|
|
||||||
elif head == "nohup":
|
elif head == "nohup":
|
||||||
# 挂起运行
|
# 挂起运行
|
||||||
@ -106,6 +120,7 @@ class LiteyukiFunction:
|
|||||||
self.end = True
|
self.end = True
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
elif head == "await":
|
elif head == "await":
|
||||||
# 等待所有协程执行完毕
|
# 等待所有协程执行完毕
|
||||||
await asyncio.gather(*self.sub_tasks)
|
await asyncio.gather(*self.sub_tasks)
|
||||||
|
@ -64,6 +64,11 @@ def load_resource_from_dir(path: str):
|
|||||||
from liteyuki.utils.base.ly_function import load_from_dir
|
from liteyuki.utils.base.ly_function import load_from_dir
|
||||||
load_from_dir(os.path.join(path, "functions"))
|
load_from_dir(os.path.join(path, "functions"))
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(path, "word_bank")):
|
||||||
|
# 加载词库
|
||||||
|
from liteyuki.utils.base.word_bank import load_from_dir
|
||||||
|
load_from_dir(os.path.join(path, "word_bank"))
|
||||||
|
|
||||||
_loaded_resource_packs.insert(0, ResourceMetadata(**metadata))
|
_loaded_resource_packs.insert(0, ResourceMetadata(**metadata))
|
||||||
|
|
||||||
|
|
||||||
|
57
liteyuki/utils/base/word_bank.py
Normal file
57
liteyuki/utils/base/word_bank.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
|
||||||
|
word_bank: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_file(file_path: str):
|
||||||
|
"""
|
||||||
|
从json文件中加载词库
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 文件路径
|
||||||
|
"""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
data = json.load(file)
|
||||||
|
for key, value_list in data.items():
|
||||||
|
if key not in word_bank:
|
||||||
|
word_bank[key] = set()
|
||||||
|
word_bank[key].update(value_list)
|
||||||
|
|
||||||
|
nonebot.logger.debug(f"Loaded word bank from {file_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_dir(dir_path: str):
|
||||||
|
"""
|
||||||
|
从目录中加载词库
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dir_path: 目录路径
|
||||||
|
"""
|
||||||
|
for file in os.listdir(dir_path):
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(dir_path, file)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
if file.endswith(".json"):
|
||||||
|
load_from_file(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
nonebot.logger.error(f"Failed to load language data from {file}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def get_reply(kws: Iterable[str]) -> str | None:
|
||||||
|
"""
|
||||||
|
获取回复
|
||||||
|
Args:
|
||||||
|
kws: 关键词
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
for kw in kws:
|
||||||
|
if kw in word_bank:
|
||||||
|
return random.choice(list(word_bank[kw]))
|
||||||
|
|
||||||
|
return None
|
@ -7,7 +7,7 @@ nb-cli~=1.4.1
|
|||||||
nonebot2[fastapi,httpx,websockets]~=2.3.0
|
nonebot2[fastapi,httpx,websockets]~=2.3.0
|
||||||
nonebot-plugin-htmlrender~=0.3.1
|
nonebot-plugin-htmlrender~=0.3.1
|
||||||
nonebot-adapter-onebot~=2.4.3
|
nonebot-adapter-onebot~=2.4.3
|
||||||
nonebot-plugin-alconna~=0.43.0
|
nonebot-plugin-alconna~=0.46.3
|
||||||
nonebot_plugin_apscheduler~=0.4.0
|
nonebot_plugin_apscheduler~=0.4.0
|
||||||
nonebot-adapter-satori~=0.11.5
|
nonebot-adapter-satori~=0.11.5
|
||||||
packaging~=23.1
|
packaging~=23.1
|
||||||
|
Loading…
Reference in New Issue
Block a user