feat(help): 重构帮助系统为图片渲染模式
添加浏览器管理器和图片管理器,用于通过 Playwright 渲染帮助菜单为图片 重构命令管理器以支持图片缓存和同步功能 添加 HTML 模板用于帮助菜单渲染
This commit is contained in:
BIN
core/data/temp/help_menu.png
Normal file
BIN
core/data/temp/help_menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -9,6 +9,8 @@ from .command_manager import matcher as command_manager
|
|||||||
from .permission_manager import PermissionManager
|
from .permission_manager import PermissionManager
|
||||||
from .plugin_manager import PluginManager
|
from .plugin_manager import PluginManager
|
||||||
from .redis_manager import RedisManager
|
from .redis_manager import RedisManager
|
||||||
|
from .browser_manager import BrowserManager
|
||||||
|
from .image_manager import ImageManager
|
||||||
|
|
||||||
# --- 实例化所有单例管理器 ---
|
# --- 实例化所有单例管理器 ---
|
||||||
|
|
||||||
@@ -28,6 +30,12 @@ plugin_manager.load_all_plugins()
|
|||||||
# Redis 管理器
|
# Redis 管理器
|
||||||
redis_manager = RedisManager()
|
redis_manager = RedisManager()
|
||||||
|
|
||||||
|
# 浏览器管理器
|
||||||
|
browser_manager = BrowserManager()
|
||||||
|
|
||||||
|
# 图片管理器
|
||||||
|
image_manager = ImageManager()
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"admin_manager",
|
"admin_manager",
|
||||||
"permission_manager",
|
"permission_manager",
|
||||||
@@ -35,4 +43,6 @@ __all__ = [
|
|||||||
"matcher",
|
"matcher",
|
||||||
"plugin_manager",
|
"plugin_manager",
|
||||||
"redis_manager",
|
"redis_manager",
|
||||||
|
"browser_manager",
|
||||||
|
"image_manager",
|
||||||
]
|
]
|
||||||
|
|||||||
72
core/managers/browser_manager.py
Normal file
72
core/managers/browser_manager.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
浏览器管理器模块
|
||||||
|
|
||||||
|
负责管理全局唯一的 Playwright 浏览器实例,避免频繁启动/关闭浏览器的开销。
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional
|
||||||
|
from playwright.async_api import async_playwright, Browser, Playwright, Page
|
||||||
|
from ..utils.logger import logger
|
||||||
|
|
||||||
|
class BrowserManager:
|
||||||
|
"""
|
||||||
|
浏览器管理器(异步单例)
|
||||||
|
"""
|
||||||
|
_instance = None
|
||||||
|
_playwright: Optional[Playwright] = None
|
||||||
|
_browser: Optional[Browser] = None
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""
|
||||||
|
初始化 Playwright 和 Browser
|
||||||
|
"""
|
||||||
|
if self._browser is None:
|
||||||
|
try:
|
||||||
|
logger.info("正在启动无头浏览器...")
|
||||||
|
self._playwright = await async_playwright().start()
|
||||||
|
# 启动 Chromium,headless=True 表示无头模式
|
||||||
|
self._browser = await self._playwright.chromium.launch(headless=True)
|
||||||
|
logger.success("无头浏览器启动成功!")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"无头浏览器启动失败: {e}")
|
||||||
|
self._browser = None
|
||||||
|
|
||||||
|
async def get_new_page(self) -> Optional[Page]:
|
||||||
|
"""
|
||||||
|
获取一个新的页面 (Page)
|
||||||
|
|
||||||
|
使用完毕后,调用者应该负责关闭该页面 (await page.close())
|
||||||
|
"""
|
||||||
|
if self._browser is None:
|
||||||
|
logger.warning("浏览器尚未初始化,尝试重新初始化...")
|
||||||
|
await self.initialize()
|
||||||
|
|
||||||
|
if self._browser:
|
||||||
|
try:
|
||||||
|
return await self._browser.new_page()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建新页面失败: {e}")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
"""
|
||||||
|
关闭浏览器和 Playwright
|
||||||
|
"""
|
||||||
|
if self._browser:
|
||||||
|
await self._browser.close()
|
||||||
|
self._browser = None
|
||||||
|
logger.info("浏览器已关闭")
|
||||||
|
|
||||||
|
if self._playwright:
|
||||||
|
await self._playwright.stop()
|
||||||
|
self._playwright = None
|
||||||
|
logger.info("Playwright 已停止")
|
||||||
|
|
||||||
|
# 全局浏览器管理器实例
|
||||||
|
browser_manager = BrowserManager()
|
||||||
@@ -7,12 +7,16 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, Optional, Tuple
|
from typing import Any, Callable, Dict, Optional, Tuple
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
from models.events.message import MessageSegment
|
from models.events.message import MessageSegment
|
||||||
|
|
||||||
from ..config_loader import global_config
|
from ..config_loader import global_config
|
||||||
from ..handlers.event_handler import MessageHandler, NoticeHandler, RequestHandler
|
from ..handlers.event_handler import MessageHandler, NoticeHandler, RequestHandler
|
||||||
from .help_pic import help_pic
|
from .redis_manager import redis_manager
|
||||||
|
from .image_manager import image_manager
|
||||||
|
from ..utils.logger import logger
|
||||||
|
|
||||||
# 从配置中获取命令前缀
|
# 从配置中获取命令前缀
|
||||||
_config_prefixes = global_config.bot.command
|
_config_prefixes = global_config.bot.command
|
||||||
@@ -59,6 +63,40 @@ class CommandManager:
|
|||||||
# 注册内置的 /help 命令
|
# 注册内置的 /help 命令
|
||||||
self._register_internal_commands()
|
self._register_internal_commands()
|
||||||
|
|
||||||
|
async def sync_help_pic(self):
|
||||||
|
"""
|
||||||
|
启动时或插件重载时同步 help 图片到 Redis
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("正在生成帮助图片...")
|
||||||
|
|
||||||
|
# 1. 收集插件数据
|
||||||
|
plugins_data = []
|
||||||
|
for plugin_name, meta in self.plugins.items():
|
||||||
|
plugins_data.append({
|
||||||
|
"name": meta.get("name", plugin_name),
|
||||||
|
"description": meta.get("description", "暂无描述"),
|
||||||
|
"usage": meta.get("usage", "暂无用法")
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. 渲染图片
|
||||||
|
# 使用 png 格式以获得更好的文字清晰度
|
||||||
|
base64_str = await image_manager.render_template_to_base64(
|
||||||
|
template_name="help.html",
|
||||||
|
data={"plugins": plugins_data},
|
||||||
|
output_name="help_menu.png",
|
||||||
|
image_type="png"
|
||||||
|
)
|
||||||
|
|
||||||
|
if base64_str:
|
||||||
|
await redis_manager.set("neobot:core:help_pic", base64_str)
|
||||||
|
logger.success("帮助图片已更新并缓存到 Redis")
|
||||||
|
else:
|
||||||
|
logger.error("帮助图片生成失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"同步帮助图片失败: {e}")
|
||||||
|
|
||||||
def _register_internal_commands(self):
|
def _register_internal_commands(self):
|
||||||
"""
|
"""
|
||||||
注册框架内置的命令
|
注册框架内置的命令
|
||||||
@@ -160,9 +198,22 @@ class CommandManager:
|
|||||||
async def _help_command(self, bot, event):
|
async def _help_command(self, bot, event):
|
||||||
"""
|
"""
|
||||||
内置的 `/help` 命令的实现。
|
内置的 `/help` 命令的实现。
|
||||||
|
直接从 Redis 获取缓存的图片。
|
||||||
"""
|
"""
|
||||||
help_text = "--- 可用指令列表 ---\n"
|
# 1. 尝试从 Redis 获取
|
||||||
|
help_pic = await redis_manager.get("neobot:core:help_pic")
|
||||||
|
|
||||||
|
if not help_pic:
|
||||||
|
await bot.send(event, "帮助图片缓存缺失,正在重新生成...")
|
||||||
|
await self.sync_help_pic()
|
||||||
|
help_pic = await redis_manager.get("neobot:core:help_pic")
|
||||||
|
|
||||||
|
if help_pic:
|
||||||
|
await bot.send(event, MessageSegment.image(help_pic))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 最后的兜底:发送纯文本
|
||||||
|
help_text = "--- 可用指令列表 ---\n"
|
||||||
for plugin_name, meta in self.plugins.items():
|
for plugin_name, meta in self.plugins.items():
|
||||||
name = meta.get("name", "未命名插件")
|
name = meta.get("name", "未命名插件")
|
||||||
description = meta.get("description", "暂无描述")
|
description = meta.get("description", "暂无描述")
|
||||||
@@ -171,9 +222,8 @@ class CommandManager:
|
|||||||
help_text += f"\n{name}:\n"
|
help_text += f"\n{name}:\n"
|
||||||
help_text += f" 功能: {description}\n"
|
help_text += f" 功能: {description}\n"
|
||||||
help_text += f" 用法: {usage}\n"
|
help_text += f" 用法: {usage}\n"
|
||||||
|
|
||||||
await bot.send(event, MessageSegment.image(help_pic))
|
await bot.send(event, help_text.strip())
|
||||||
# await bot.send(event, help_text.strip())
|
|
||||||
|
|
||||||
|
|
||||||
# 实例化全局唯一的命令管理器
|
# 实例化全局唯一的命令管理器
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
115
core/managers/image_manager.py
Normal file
115
core/managers/image_manager.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
图片生成管理器模块
|
||||||
|
|
||||||
|
负责管理图片生成相关的逻辑,支持多种渲染引擎(目前支持 Playwright)。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from .browser_manager import browser_manager
|
||||||
|
from ..utils.logger import logger
|
||||||
|
|
||||||
|
class ImageManager:
|
||||||
|
"""
|
||||||
|
图片生成管理器(单例)
|
||||||
|
"""
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 模板目录
|
||||||
|
self.template_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "templates")
|
||||||
|
# 临时文件目录
|
||||||
|
# core/managers/image_manager.py -> core/managers -> core -> core/data/temp
|
||||||
|
self.temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "temp")
|
||||||
|
os.makedirs(self.temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
async def render_template(self, template_name: str, data: Dict[str, Any], output_name: str = "output.png", quality: int = 80, image_type: str = "png") -> Optional[str]:
|
||||||
|
"""
|
||||||
|
使用 Playwright 渲染 Jinja2 模板并保存为图片文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_name (str): 模板文件名 (例如 "help.html")
|
||||||
|
data (Dict[str, Any]): 传递给模板的数据字典
|
||||||
|
output_name (str, optional): 输出文件名. Defaults to "output.png".
|
||||||
|
quality (int, optional): JPEG 质量 (0-100). 仅在 image_type 为 jpeg 时有效. Defaults to 80.
|
||||||
|
image_type (str, optional): 图片类型 ('png' or 'jpeg'). Defaults to "png".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: 生成图片的绝对路径,如果失败则返回 None
|
||||||
|
"""
|
||||||
|
template_path = os.path.join(self.template_dir, template_name)
|
||||||
|
if not os.path.exists(template_path):
|
||||||
|
logger.error(f"模板文件未找到: {template_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 渲染 HTML
|
||||||
|
with open(template_path, "r", encoding="utf-8") as f:
|
||||||
|
template_str = f.read()
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
html_content = template.render(**data)
|
||||||
|
|
||||||
|
# 2. 使用浏览器截图
|
||||||
|
page = await browser_manager.get_new_page()
|
||||||
|
if not page:
|
||||||
|
logger.error("无法获取浏览器页面")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 设置视口
|
||||||
|
await page.set_viewport_size({"width": 650, "height": 100})
|
||||||
|
|
||||||
|
# 加载内容
|
||||||
|
await page.set_content(html_content)
|
||||||
|
await page.wait_for_selector("body")
|
||||||
|
|
||||||
|
# 截图
|
||||||
|
screenshot_args = {'full_page': True, 'type': image_type}
|
||||||
|
if image_type == 'jpeg':
|
||||||
|
screenshot_args['quality'] = quality
|
||||||
|
|
||||||
|
screenshot_bytes = await page.screenshot(**screenshot_args)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
# 3. 保存文件
|
||||||
|
output_path = os.path.join(self.temp_dir, output_name)
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(screenshot_bytes)
|
||||||
|
|
||||||
|
logger.info(f"图片已生成: {output_path} ({len(screenshot_bytes)/1024:.2f} KB)")
|
||||||
|
return os.path.abspath(output_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"渲染模板 {template_name} 失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def render_template_to_base64(self, template_name: str, data: Dict[str, Any], output_name: str = "output.png", quality: int = 80, image_type: str = "png") -> Optional[str]:
|
||||||
|
"""
|
||||||
|
渲染模板并返回 Base64 编码的图片字符串
|
||||||
|
"""
|
||||||
|
file_path = await self.render_template(template_name, data, output_name, quality, image_type)
|
||||||
|
if not file_path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
mime_type = "image/jpeg" if image_type == "jpeg" else "image/png"
|
||||||
|
return f"data:{mime_type};base64," + base64.b64encode(content).decode("utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"读取图片文件失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 全局图片管理器实例
|
||||||
|
image_manager = ImageManager()
|
||||||
18
main.py
18
main.py
@@ -15,8 +15,9 @@ from core.utils.logger import logger
|
|||||||
|
|
||||||
from core.managers.admin_manager import admin_manager
|
from core.managers.admin_manager import admin_manager
|
||||||
from core.ws import WS
|
from core.ws import WS
|
||||||
from core.managers import plugin_manager
|
from core.managers import plugin_manager, matcher
|
||||||
from core.managers.redis_manager import redis_manager
|
from core.managers.redis_manager import redis_manager
|
||||||
|
from core.managers.browser_manager import browser_manager
|
||||||
from core.utils.executor import run_in_thread_pool, initialize_executor
|
from core.utils.executor import run_in_thread_pool, initialize_executor
|
||||||
from core.config_loader import global_config as config
|
from core.config_loader import global_config as config
|
||||||
|
|
||||||
@@ -29,6 +30,15 @@ sys.path.insert(0, ROOT_DIR)
|
|||||||
PLUGIN_DIR = os.path.join(ROOT_DIR, "plugins")
|
PLUGIN_DIR = os.path.join(ROOT_DIR, "plugins")
|
||||||
|
|
||||||
|
|
||||||
|
async def reload_plugin_and_sync_help(module_name: str):
|
||||||
|
"""
|
||||||
|
重载插件并同步帮助图片
|
||||||
|
"""
|
||||||
|
await run_in_thread_pool(plugin_manager.reload_plugin, module_name)
|
||||||
|
# 插件重载后,重新生成帮助图片
|
||||||
|
await matcher.sync_help_pic()
|
||||||
|
|
||||||
|
|
||||||
class PluginReloadHandler(FileSystemEventHandler):
|
class PluginReloadHandler(FileSystemEventHandler):
|
||||||
"""
|
"""
|
||||||
文件变更处理器,用于热重载插件
|
文件变更处理器,用于热重载插件
|
||||||
@@ -102,9 +112,15 @@ async def main():
|
|||||||
# 初始化 Redis 连接
|
# 初始化 Redis 连接
|
||||||
await redis_manager.initialize()
|
await redis_manager.initialize()
|
||||||
|
|
||||||
|
# 同步帮助图片
|
||||||
|
await matcher.sync_help_pic()
|
||||||
|
|
||||||
# 初始化管理员管理器
|
# 初始化管理员管理器
|
||||||
await admin_manager.initialize()
|
await admin_manager.initialize()
|
||||||
|
|
||||||
|
# 初始化浏览器管理器
|
||||||
|
await browser_manager.initialize()
|
||||||
|
|
||||||
# 启动文件监控
|
# 启动文件监控
|
||||||
# 监控 plugins 目录
|
# 监控 plugins 目录
|
||||||
plugin_path = os.path.join(os.path.dirname(__file__), "plugins")
|
plugin_path = os.path.join(os.path.dirname(__file__), "plugins")
|
||||||
|
|||||||
134
templates/help.html
Normal file
134
templates/help.html
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NeoBot 帮助菜单</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #4a90e2;
|
||||||
|
--bg-color: #f5f7fa;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text-color: #333333;
|
||||||
|
--secondary-text: #666666;
|
||||||
|
--border-radius: 12px;
|
||||||
|
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Microsoft YaHei', 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5em;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
border-left: 5px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-name {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-desc {
|
||||||
|
color: var(--secondary-text);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-usage {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #d63384;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
color: var(--secondary-text);
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>NeoBot</h1>
|
||||||
|
<p>功能插件列表</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plugin-list">
|
||||||
|
{% for plugin in plugins %}
|
||||||
|
<div class="plugin-card">
|
||||||
|
<div class="plugin-header">
|
||||||
|
<span class="plugin-name">{{ plugin.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-desc">{{ plugin.description }}</div>
|
||||||
|
<div class="plugin-usage">
|
||||||
|
{{ plugin.usage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Generated by NeoBot • Playwright Rendering
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user