random qa
This commit is contained in:
@@ -18,16 +18,12 @@ from src.api.apiforgui import (
|
|||||||
from src.chat.heart_flow.sub_heartflow import ChatState
|
from src.chat.heart_flow.sub_heartflow import ChatState
|
||||||
from src.api.basic_info_api import get_all_basic_info # 新增导入
|
from src.api.basic_info_api import get_all_basic_info # 新增导入
|
||||||
|
|
||||||
# import uvicorn
|
|
||||||
# import os
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("api")
|
logger = get_logger("api")
|
||||||
|
|
||||||
# maiapi = FastAPI()
|
|
||||||
logger.info("麦麦API服务器已启动")
|
logger.info("麦麦API服务器已启动")
|
||||||
graphql_router = GraphQLRouter(schema=None, path="/") # Replace `None` with your actual schema
|
graphql_router = GraphQLRouter(schema=None, path="/") # Replace `None` with your actual schema
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from rich.traceback import install
|
from rich.traceback import install
|
||||||
from src.config.config import Config
|
from src.config.config import get_config_dir, load_config
|
||||||
from src.common.logger_manager import get_logger
|
from src.common.logger_manager import get_logger
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ async def reload_config():
|
|||||||
from src.config import config as config_module
|
from src.config import config as config_module
|
||||||
|
|
||||||
logger.debug("正在重载配置文件...")
|
logger.debug("正在重载配置文件...")
|
||||||
bot_config_path = os.path.join(Config.get_config_dir(), "bot_config.toml")
|
bot_config_path = os.path.join(get_config_dir(), "bot_config.toml")
|
||||||
config_module.global_config = Config.load_config(config_path=bot_config_path)
|
config_module.global_config = load_config(config_path=bot_config_path)
|
||||||
logger.debug("配置文件重载成功")
|
logger.debug("配置文件重载成功")
|
||||||
return {"status": "reloaded"}
|
return {"status": "reloaded"}
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ class BaseAction(ABC):
|
|||||||
"""初始化动作
|
"""初始化动作
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action_name: 动作名称
|
|
||||||
action_data: 动作数据
|
action_data: 动作数据
|
||||||
reasoning: 执行该动作的理由
|
reasoning: 执行该动作的理由
|
||||||
cycle_timers: 计时器字典
|
cycle_timers: 计时器字典
|
||||||
|
|||||||
@@ -118,13 +118,10 @@ class ReplyAction(BaseAction):
|
|||||||
|
|
||||||
reply_to = reply_data.get("reply_to", "none")
|
reply_to = reply_data.get("reply_to", "none")
|
||||||
|
|
||||||
# sender = ""
|
|
||||||
target = ""
|
|
||||||
if ":" in reply_to or ":" in reply_to:
|
if ":" in reply_to or ":" in reply_to:
|
||||||
# 使用正则表达式匹配中文或英文冒号
|
# 使用正则表达式匹配中文或英文冒号
|
||||||
parts = re.split(pattern=r"[::]", string=reply_to, maxsplit=1)
|
parts = re.split(pattern=r"[::]", string=reply_to, maxsplit=1)
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
# sender = parts[0].strip()
|
|
||||||
target = parts[1].strip()
|
target = parts[1].strip()
|
||||||
anchor_message = chatting_observation.search_message_by_text(target)
|
anchor_message = chatting_observation.search_message_by_text(target)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ class BaseCommand(ABC):
|
|||||||
"""
|
"""
|
||||||
self.message = message
|
self.message = message
|
||||||
self.matched_groups: Dict[str, str] = {} # 存储正则表达式匹配的命名组
|
self.matched_groups: Dict[str, str] = {} # 存储正则表达式匹配的命名组
|
||||||
self._services = {} # 存储内部服务
|
self._services = {"chat_stream": message.chat_stream} # 存储内部服务
|
||||||
|
|
||||||
# 设置服务
|
|
||||||
self._services["chat_stream"] = message.chat_stream
|
|
||||||
|
|
||||||
# 日志前缀
|
# 日志前缀
|
||||||
self.log_prefix = f"[Command:{self.command_name}]"
|
self.log_prefix = f"[Command:{self.command_name}]"
|
||||||
|
|||||||
@@ -181,11 +181,6 @@ class DefaultExpressor:
|
|||||||
(已整合原 HeartFCGenerator 的功能)
|
(已整合原 HeartFCGenerator 的功能)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 获取情绪影响因子并调整模型温度
|
|
||||||
# arousal_multiplier = mood_manager.get_arousal_multiplier()
|
|
||||||
# current_temp = float(global_config.model.normal["temp"]) * arousal_multiplier
|
|
||||||
# self.express_model.params["temperature"] = current_temp # 动态调整温度
|
|
||||||
|
|
||||||
# --- Determine sender_name for private chat ---
|
# --- Determine sender_name for private chat ---
|
||||||
sender_name_for_prompt = "某人" # Default for group or if info unavailable
|
sender_name_for_prompt = "某人" # Default for group or if info unavailable
|
||||||
if not self.is_group_chat and self.chat_target_info:
|
if not self.is_group_chat and self.chat_target_info:
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ class ToolProcessor(BaseProcessor):
|
|||||||
self.structured_info = []
|
self.structured_info = []
|
||||||
|
|
||||||
async def process_info(
|
async def process_info(
|
||||||
self, observations: Optional[List[Observation]] = None, running_memorys: Optional[List[Dict]] = None, *infos
|
self, observations: Optional[List[Observation]] = None, running_memories: Optional[List[Dict]] = None, *infos
|
||||||
) -> List[dict]:
|
) -> List[StructuredInfo]:
|
||||||
"""处理信息对象
|
"""处理信息对象
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
observations: 可选的观察列表,包含ChattingObservation和StructureObservation类型
|
||||||
|
running_memories: 可选的运行时记忆列表,包含字典类型的记忆信息
|
||||||
*infos: 可变数量的InfoBase类型的信息对象
|
*infos: 可变数量的InfoBase类型的信息对象
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -60,15 +62,15 @@ class ToolProcessor(BaseProcessor):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
working_infos = []
|
working_infos = []
|
||||||
|
result = []
|
||||||
|
|
||||||
if observations:
|
if observations:
|
||||||
for observation in observations:
|
for observation in observations:
|
||||||
if isinstance(observation, ChattingObservation):
|
if isinstance(observation, ChattingObservation):
|
||||||
result, used_tools, prompt = await self.execute_tools(observation, running_memorys)
|
result, used_tools, prompt = await self.execute_tools(observation, running_memories)
|
||||||
|
|
||||||
# 更新WorkingObservation中的结构化信息
|
|
||||||
logger.debug(f"工具调用结果: {result}")
|
logger.debug(f"工具调用结果: {result}")
|
||||||
|
# 更新WorkingObservation中的结构化信息
|
||||||
for observation in observations:
|
for observation in observations:
|
||||||
if isinstance(observation, StructureObservation):
|
if isinstance(observation, StructureObservation):
|
||||||
for structured_info in result:
|
for structured_info in result:
|
||||||
@@ -81,12 +83,7 @@ class ToolProcessor(BaseProcessor):
|
|||||||
structured_info = StructuredInfo()
|
structured_info = StructuredInfo()
|
||||||
if working_infos:
|
if working_infos:
|
||||||
for working_info in working_infos:
|
for working_info in working_infos:
|
||||||
# print(f"working_info: {working_info}")
|
|
||||||
# print(f"working_info.get('type'): {working_info.get('type')}")
|
|
||||||
# print(f"working_info.get('content'): {working_info.get('content')}")
|
|
||||||
structured_info.set_info(key=working_info.get("type"), value=working_info.get("content"))
|
structured_info.set_info(key=working_info.get("type"), value=working_info.get("content"))
|
||||||
# info = structured_info.get_processed_info()
|
|
||||||
# print(f"info: {info}")
|
|
||||||
|
|
||||||
return [structured_info]
|
return [structured_info]
|
||||||
|
|
||||||
|
|||||||
@@ -198,9 +198,7 @@ class ActionModifier:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
actions_with_info: 带完整信息的动作字典
|
actions_with_info: 带完整信息的动作字典
|
||||||
observed_messages_str: 观察到的聊天消息
|
chat_content: 聊天内容
|
||||||
chat_context: 聊天上下文信息
|
|
||||||
extra_context: 额外的上下文信息
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: 过滤后激活的actions字典
|
Dict[str, Any]: 过滤后激活的actions字典
|
||||||
@@ -320,9 +318,7 @@ class ActionModifier:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
llm_judge_actions: 需要LLM判定的actions
|
llm_judge_actions: 需要LLM判定的actions
|
||||||
observed_messages_str: 观察到的聊天消息
|
chat_content: 聊天内容
|
||||||
chat_context: 聊天上下文
|
|
||||||
extra_context: 额外上下文
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, bool]: action名称到激活结果的映射
|
Dict[str, bool]: action名称到激活结果的映射
|
||||||
|
|||||||
@@ -217,7 +217,6 @@ class ActionPlanner(BasePlanner):
|
|||||||
|
|
||||||
# 提取决策,提供默认值
|
# 提取决策,提供默认值
|
||||||
extracted_action = parsed_json.get("action", "no_reply")
|
extracted_action = parsed_json.get("action", "no_reply")
|
||||||
# extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
|
|
||||||
extracted_reasoning = ""
|
extracted_reasoning = ""
|
||||||
|
|
||||||
# 将所有其他属性添加到action_data
|
# 将所有其他属性添加到action_data
|
||||||
|
|||||||
@@ -132,9 +132,6 @@ global_config = dict(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# _load_config(global_config, parser.parse_args().config_path)
|
|
||||||
# file_path = os.path.abspath(__file__)
|
|
||||||
# dir_path = os.path.dirname(file_path)
|
|
||||||
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||||
config_path = os.path.join(ROOT_PATH, "config", "lpmm_config.toml")
|
config_path = os.path.join(ROOT_PATH, "config", "lpmm_config.toml")
|
||||||
_load_config(global_config, config_path)
|
_load_config(global_config, config_path)
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ def load_raw_data(path: str = None) -> tuple[list[str], list[str]]:
|
|||||||
import_json = json.loads(f.read())
|
import_json = json.loads(f.read())
|
||||||
else:
|
else:
|
||||||
raise Exception(f"原始数据文件读取失败: {json_path}")
|
raise Exception(f"原始数据文件读取失败: {json_path}")
|
||||||
# import_json内容示例:
|
"""
|
||||||
# import_json = [
|
import_json 内容示例:
|
||||||
# "The capital of China is Beijing. The capital of France is Paris.",
|
import_json = ["The capital of China is Beijing. The capital of France is Paris.",]
|
||||||
# ]
|
"""
|
||||||
raw_data = []
|
raw_data = []
|
||||||
sha256_list = []
|
sha256_list = []
|
||||||
sha256_set = set()
|
sha256_set = set()
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class MessageRecv(Message):
|
|||||||
self.detailed_plain_text = message_dict.get("detailed_plain_text", "") # 初始化为空字符串
|
self.detailed_plain_text = message_dict.get("detailed_plain_text", "") # 初始化为空字符串
|
||||||
self.is_emoji = False
|
self.is_emoji = False
|
||||||
|
|
||||||
def update_chat_stream(self, chat_stream: "ChatStream"):
|
def update_chat_stream(self, chat_stream: ChatStream):
|
||||||
self.chat_stream = chat_stream
|
self.chat_stream = chat_stream
|
||||||
|
|
||||||
async def process(self) -> None:
|
async def process(self) -> None:
|
||||||
|
|||||||
@@ -585,14 +585,9 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
|
|||||||
|
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
try:
|
try:
|
||||||
# user_info = msg.get("user_info", {})
|
|
||||||
platform = msg.get("chat_info_platform")
|
platform = msg.get("chat_info_platform")
|
||||||
user_id = msg.get("user_id")
|
user_id = msg.get("user_id")
|
||||||
_timestamp = msg.get("time")
|
_timestamp = msg.get("time")
|
||||||
# print(f"msg:{msg}")
|
|
||||||
# print(f"platform:{platform}")
|
|
||||||
# print(f"user_id:{user_id}")
|
|
||||||
# print(f"timestamp:{timestamp}")
|
|
||||||
if msg.get("display_message"):
|
if msg.get("display_message"):
|
||||||
content = msg.get("display_message")
|
content = msg.get("display_message")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -247,8 +247,6 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
|
|||||||
|
|
||||||
# 如果分割后为空(例如,输入全是分隔符且不满足保留条件),恢复颜文字并返回
|
# 如果分割后为空(例如,输入全是分隔符且不满足保留条件),恢复颜文字并返回
|
||||||
if not segments:
|
if not segments:
|
||||||
# recovered_text = recover_kaomoji([text], mapping) # 恢复原文本中的颜文字 - 已移至上层处理
|
|
||||||
# return [s for s in recovered_text if s] # 返回非空结果
|
|
||||||
return [text] if text else [] # 如果原始文本非空,则返回原始文本(可能只包含未被分割的字符或颜文字占位符)
|
return [text] if text else [] # 如果原始文本非空,则返回原始文本(可能只包含未被分割的字符或颜文字占位符)
|
||||||
|
|
||||||
# 2. 概率合并
|
# 2. 概率合并
|
||||||
@@ -336,7 +334,6 @@ def process_llm_response(text: str) -> list[str]:
|
|||||||
kaomoji_mapping = {}
|
kaomoji_mapping = {}
|
||||||
# 提取被 () 或 [] 或 ()包裹且包含中文的内容
|
# 提取被 () 或 [] 或 ()包裹且包含中文的内容
|
||||||
pattern = re.compile(r"[(\[(](?=.*[一-鿿]).*?[)\])]")
|
pattern = re.compile(r"[(\[(](?=.*[一-鿿]).*?[)\])]")
|
||||||
# _extracted_contents = pattern.findall(text)
|
|
||||||
_extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
|
_extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
|
||||||
# 去除 () 和 [] 及其包裹的内容
|
# 去除 () 和 [] 及其包裹的内容
|
||||||
cleaned_text = pattern.sub("", protected_text)
|
cleaned_text = pattern.sub("", protected_text)
|
||||||
|
|||||||
@@ -185,6 +185,14 @@ def load_config(config_path: str) -> Config:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_dir() -> str:
|
||||||
|
"""
|
||||||
|
获取配置目录
|
||||||
|
:return: 配置目录路径
|
||||||
|
"""
|
||||||
|
return CONFIG_DIR
|
||||||
|
|
||||||
|
|
||||||
# 获取配置文件路径
|
# 获取配置文件路径
|
||||||
logger.info(f"MaiCore当前版本: {MMC_VERSION}")
|
logger.info(f"MaiCore当前版本: {MMC_VERSION}")
|
||||||
update_config()
|
update_config()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from typing import List, Dict, Any, Callable
|
|||||||
|
|
||||||
from playhouse import shortcuts
|
from playhouse import shortcuts
|
||||||
|
|
||||||
# from src.common.database.database import db # Peewee db 导入
|
|
||||||
from src.common.database.database_model import Messages # Peewee Messages 模型导入
|
from src.common.database.database_model import Messages # Peewee Messages 模型导入
|
||||||
|
|
||||||
model_to_dict: Callable[..., dict] = shortcuts.model_to_dict # Peewee 模型转换为字典的快捷函数
|
model_to_dict: Callable[..., dict] = shortcuts.model_to_dict # Peewee 模型转换为字典的快捷函数
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ class PersonalityExpression:
|
|||||||
def _read_meta_data(self):
|
def _read_meta_data(self):
|
||||||
if os.path.exists(self.meta_file_path):
|
if os.path.exists(self.meta_file_path):
|
||||||
try:
|
try:
|
||||||
with open(self.meta_file_path, "r", encoding="utf-8") as f:
|
with open(self.meta_file_path, "r", encoding="utf-8") as meta_file:
|
||||||
meta_data = json.load(f)
|
meta_data = json.load(meta_file)
|
||||||
# 检查是否有last_update_time字段
|
# 检查是否有last_update_time字段
|
||||||
if "last_update_time" not in meta_data:
|
if "last_update_time" not in meta_data:
|
||||||
logger.warning(f"{self.meta_file_path} 中缺少last_update_time字段,将重新开始。")
|
logger.warning(f"{self.meta_file_path} 中缺少last_update_time字段,将重新开始。")
|
||||||
@@ -57,8 +57,8 @@ class PersonalityExpression:
|
|||||||
self._write_meta_data({"last_style_text": None, "count": 0, "last_update_time": None})
|
self._write_meta_data({"last_style_text": None, "count": 0, "last_update_time": None})
|
||||||
# 清空并重写表达文件
|
# 清空并重写表达文件
|
||||||
if os.path.exists(self.expressions_file_path):
|
if os.path.exists(self.expressions_file_path):
|
||||||
with open(self.expressions_file_path, "w", encoding="utf-8") as f:
|
with open(self.expressions_file_path, "w", encoding="utf-8") as expressions_file:
|
||||||
json.dump([], f, ensure_ascii=False, indent=2)
|
json.dump([], expressions_file, ensure_ascii=False, indent=2)
|
||||||
logger.debug(f"已清空表达文件: {self.expressions_file_path}")
|
logger.debug(f"已清空表达文件: {self.expressions_file_path}")
|
||||||
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
||||||
return meta_data
|
return meta_data
|
||||||
@@ -68,16 +68,16 @@ class PersonalityExpression:
|
|||||||
self._write_meta_data({"last_style_text": None, "count": 0, "last_update_time": None})
|
self._write_meta_data({"last_style_text": None, "count": 0, "last_update_time": None})
|
||||||
# 清空并重写表达文件
|
# 清空并重写表达文件
|
||||||
if os.path.exists(self.expressions_file_path):
|
if os.path.exists(self.expressions_file_path):
|
||||||
with open(self.expressions_file_path, "w", encoding="utf-8") as f:
|
with open(self.expressions_file_path, "w", encoding="utf-8") as expressions_file:
|
||||||
json.dump([], f, ensure_ascii=False, indent=2)
|
json.dump([], expressions_file, ensure_ascii=False, indent=2)
|
||||||
logger.debug(f"已清空表达文件: {self.expressions_file_path}")
|
logger.debug(f"已清空表达文件: {self.expressions_file_path}")
|
||||||
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
||||||
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
||||||
|
|
||||||
def _write_meta_data(self, data):
|
def _write_meta_data(self, data):
|
||||||
os.makedirs(os.path.dirname(self.meta_file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.meta_file_path), exist_ok=True)
|
||||||
with open(self.meta_file_path, "w", encoding="utf-8") as f:
|
with open(self.meta_file_path, "w", encoding="utf-8") as meta_file:
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, meta_file, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
async def extract_and_store_personality_expressions(self):
|
async def extract_and_store_personality_expressions(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ class MessageInfoCommand(BaseCommand):
|
|||||||
"",
|
"",
|
||||||
"🔄 聊天流信息:",
|
"🔄 聊天流信息:",
|
||||||
f" 流ID: {chat_stream.stream_id}",
|
f" 流ID: {chat_stream.stream_id}",
|
||||||
f" 是否激活: {'是' if chat_stream.is_active else '否'}",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -172,7 +171,6 @@ class MessageInfoCommand(BaseCommand):
|
|||||||
"🔄 聊天流详细信息:",
|
"🔄 聊天流详细信息:",
|
||||||
f" 流ID: {chat_stream.stream_id}",
|
f" 流ID: {chat_stream.stream_id}",
|
||||||
f" 平台: {chat_stream.platform}",
|
f" 平台: {chat_stream.platform}",
|
||||||
f" 是否激活: {'是' if chat_stream.is_active else '否'}",
|
|
||||||
f" 用户信息: {chat_stream.user_info.user_nickname} ({chat_stream.user_info.user_id})",
|
f" 用户信息: {chat_stream.user_info.user_nickname} ({chat_stream.user_info.user_id})",
|
||||||
f" 群信息: {getattr(chat_stream.group_info, 'group_name', '私聊') if chat_stream.group_info else '私聊'}",
|
f" 群信息: {getattr(chat_stream.group_info, 'group_name', '私聊') if chat_stream.group_info else '私聊'}",
|
||||||
]
|
]
|
||||||
@@ -270,7 +268,6 @@ class ChatStreamInfoCommand(BaseCommand):
|
|||||||
"🔄 聊天流信息",
|
"🔄 聊天流信息",
|
||||||
f"🆔 流ID: {chat_stream.stream_id}",
|
f"🆔 流ID: {chat_stream.stream_id}",
|
||||||
f"🌐 平台: {chat_stream.platform}",
|
f"🌐 平台: {chat_stream.platform}",
|
||||||
f"⚡ 状态: {'激活' if chat_stream.is_active else '非激活'}",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# 用户信息
|
# 用户信息
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ class MessageContextCommand(BaseCommand):
|
|||||||
"",
|
"",
|
||||||
"🔄 聊天流:",
|
"🔄 聊天流:",
|
||||||
f" 流ID: {chat_stream.stream_id}",
|
f" 流ID: {chat_stream.stream_id}",
|
||||||
f" 激活状态: {'激活' if chat_stream.is_active else '非激活'}",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user