nonebot-plugin-l4d2-server 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_l4d2_server/__main__.py +67 -34
- nonebot_plugin_l4d2_server/config.py +53 -4
- nonebot_plugin_l4d2_server/l4_help/Help.json +2 -39
- nonebot_plugin_l4d2_server/l4_help/__init__.py +5 -7
- nonebot_plugin_l4d2_server/l4_image/html_img.py +1 -1
- nonebot_plugin_l4d2_server/l4_local/__init__.py +0 -1
- nonebot_plugin_l4d2_server/l4_request/__init__.py +36 -104
- nonebot_plugin_l4d2_server/l4_request/draw_msg.py +172 -35
- nonebot_plugin_l4d2_server/l4_request/utils.py +176 -0
- nonebot_plugin_l4d2_server/utils/api/models.py +1 -1
- nonebot_plugin_l4d2_server/utils/api/request.py +71 -37
- nonebot_plugin_l4d2_server/utils/api/utils.py +30 -0
- nonebot_plugin_l4d2_server/utils/utils.py +11 -2
- {nonebot_plugin_l4d2_server-1.0.3.dist-info → nonebot_plugin_l4d2_server-1.0.5.dist-info}/METADATA +19 -16
- {nonebot_plugin_l4d2_server-1.0.3.dist-info → nonebot_plugin_l4d2_server-1.0.5.dist-info}/RECORD +18 -19
- nonebot_plugin_l4d2_server/l4_anne/__init__.py +0 -125
- nonebot_plugin_l4d2_server/l4_anne/ranne.py +0 -18
- {nonebot_plugin_l4d2_server-1.0.3.dist-info → nonebot_plugin_l4d2_server-1.0.5.dist-info}/WHEEL +0 -0
- {nonebot_plugin_l4d2_server-1.0.3.dist-info → nonebot_plugin_l4d2_server-1.0.5.dist-info}/entry_points.txt +0 -0
- {nonebot_plugin_l4d2_server-1.0.3.dist-info → nonebot_plugin_l4d2_server-1.0.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,56 +1,193 @@
|
|
1
1
|
import asyncio
|
2
2
|
|
3
3
|
# from logging import log
|
4
|
+
import io
|
5
|
+
from pathlib import Path
|
4
6
|
from typing import List, Tuple
|
5
7
|
|
8
|
+
from nonebot.log import logger
|
9
|
+
from PIL import Image, ImageDraw, ImageFont
|
10
|
+
|
6
11
|
from ..config import config
|
7
12
|
from ..utils.api.models import NserverOut, OutServer
|
8
13
|
from ..utils.api.request import L4API
|
9
14
|
|
10
15
|
|
11
|
-
async def draw_one_ip(host: str, port: int):
|
16
|
+
async def draw_one_ip(host: str, port: int, is_img: bool = config.l4_image):
|
12
17
|
"""输出单个ip"""
|
13
|
-
# 先用文字凑合
|
14
18
|
try:
|
15
19
|
ser_list = await L4API.a2s_info([(host, port)], is_player=True)
|
16
20
|
except asyncio.exceptions.TimeoutError:
|
17
21
|
return "服务器无响应"
|
18
|
-
player_msg = ""
|
19
22
|
one_server = ser_list[0][0]
|
20
23
|
one_player = ser_list[0][1]
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
25
|
+
async def format_player_info(players: list) -> str:
|
26
|
+
"""格式化玩家信息
|
27
|
+
Args:
|
28
|
+
players: 玩家对象列表
|
29
|
+
Returns:
|
30
|
+
格式化后的玩家信息字符串
|
31
|
+
"""
|
32
|
+
player_msg = ""
|
33
|
+
if len(players):
|
34
|
+
max_duration_len = max(
|
35
|
+
[len(str(await convert_duration(i.duration))) for i in players],
|
36
|
+
)
|
37
|
+
max_score_len = max(len(str(i.score)) for i in players)
|
38
|
+
|
39
|
+
for player in players:
|
40
|
+
soc = "[{:>{}}]".format(player.score, max_score_len)
|
41
|
+
chines_dur = await convert_duration(player.duration)
|
42
|
+
dur = "{:^{}}".format(chines_dur, max_duration_len)
|
43
|
+
name_leg = len(player.name)
|
44
|
+
if name_leg > 2:
|
45
|
+
xing = ":)" * (name_leg - 2)
|
46
|
+
name = f"{player.name[0]}{xing}{player.name[-1]}"
|
47
|
+
else:
|
48
|
+
name = player.name
|
49
|
+
player_msg += f"{soc} | {dur} | {name} \n"
|
50
|
+
else:
|
51
|
+
player_msg = "服务器感觉很安静啊"
|
52
|
+
return player_msg
|
53
|
+
|
54
|
+
def build_server_message(server, player_info: str) -> str:
|
55
|
+
"""构建服务器信息消息
|
56
|
+
Args:
|
57
|
+
server: 服务器对象
|
58
|
+
player_info: 格式化后的玩家信息字符串
|
59
|
+
Returns:
|
60
|
+
完整的服务器信息字符串
|
61
|
+
"""
|
62
|
+
msg = f"""-{server.server_name}-
|
63
|
+
游戏: {server.folder}
|
64
|
+
地图: {server.map_name}
|
65
|
+
人数: {server.player_count}/{server.max_players}"""
|
66
|
+
if server.ping is not None:
|
67
|
+
msg += f"""
|
68
|
+
ping: {server.ping * 1000:.0f}ms
|
69
|
+
{player_info}"""
|
70
|
+
if config.l4_show_ip:
|
71
|
+
msg += f"""
|
52
72
|
connect {host}:{port}"""
|
53
|
-
|
73
|
+
return msg
|
74
|
+
|
75
|
+
def draw_text_on_image(
|
76
|
+
text: str,
|
77
|
+
font: ImageFont.FreeTypeFont,
|
78
|
+
draw: ImageDraw.ImageDraw,
|
79
|
+
) -> Image.Image: # type: ignore # 明确返回类型
|
80
|
+
"""在图片上绘制文本
|
81
|
+
Args:
|
82
|
+
text: 要绘制的文本
|
83
|
+
font: PIL字体对象
|
84
|
+
draw: PIL绘图对象
|
85
|
+
Returns:
|
86
|
+
包含绘制文本的PIL图片对象
|
87
|
+
"""
|
88
|
+
try:
|
89
|
+
# 分割文本为标题行和内容行
|
90
|
+
lines = text.split("\n")
|
91
|
+
if not lines:
|
92
|
+
return Image.new("RGB", (600, 400), color=(73, 109, 137))
|
93
|
+
|
94
|
+
title = lines[0]
|
95
|
+
content = "\n".join(lines[1:]) if len(lines) > 1 else ""
|
96
|
+
|
97
|
+
# 计算各部分文字尺寸
|
98
|
+
title_bbox = font.getbbox(title)
|
99
|
+
title_width = title_bbox[2] - title_bbox[0]
|
100
|
+
title_height = title_bbox[3] - title_bbox[1]
|
101
|
+
|
102
|
+
content_width = 0
|
103
|
+
content_height = 0
|
104
|
+
if content:
|
105
|
+
content_lines = content.split("\n")
|
106
|
+
line_height = font.getbbox("A")[3] - font.getbbox("A")[1]
|
107
|
+
content_height = len(content_lines) * line_height
|
108
|
+
content_width = max(
|
109
|
+
font.getbbox(line)[2] - font.getbbox(line)[0]
|
110
|
+
for line in content_lines
|
111
|
+
)
|
112
|
+
|
113
|
+
# 计算图片尺寸
|
114
|
+
margin = 20
|
115
|
+
line_spacing = 5
|
116
|
+
img_width = max(title_width, content_width) + 2 * margin
|
117
|
+
content_lines_count = len(content.split("\n")) if content else 0
|
118
|
+
img_height = max(
|
119
|
+
title_height
|
120
|
+
+ content_height
|
121
|
+
+ (line_spacing + 1) * max(0, content_lines_count - 1)
|
122
|
+
+ 2 * margin,
|
123
|
+
300,
|
124
|
+
)
|
125
|
+
|
126
|
+
# 加载背景图片
|
127
|
+
bg_path = (
|
128
|
+
Path(__file__).parent.parent / "l4_image" / "img" / "anne" / "back.png"
|
129
|
+
)
|
130
|
+
try:
|
131
|
+
img = Image.open(bg_path)
|
132
|
+
# 调整背景图尺寸
|
133
|
+
img = img.resize((int(img_width), int(img_height)))
|
134
|
+
logger.info(f"图片像素大小: {img.width}x{img.height}")
|
135
|
+
draw = ImageDraw.Draw(img)
|
136
|
+
|
137
|
+
title_x = (img_width - title_width) // 2
|
138
|
+
title_y = margin
|
139
|
+
draw.text((title_x, title_y), title, font=font, fill=(255, 255, 255))
|
140
|
+
|
141
|
+
if content:
|
142
|
+
content_x = margin
|
143
|
+
content_y = title_y + title_height + margin
|
144
|
+
draw.text(
|
145
|
+
(content_x, content_y),
|
146
|
+
content,
|
147
|
+
font=font,
|
148
|
+
fill=(255, 255, 255),
|
149
|
+
spacing=line_spacing,
|
150
|
+
)
|
151
|
+
|
152
|
+
return img
|
153
|
+
except Exception as e:
|
154
|
+
logger.error(f"加载背景图片失败: {e}")
|
155
|
+
img = Image.new(
|
156
|
+
"RGB",
|
157
|
+
(int(img_width), int(img_height)),
|
158
|
+
color=(73, 109, 137),
|
159
|
+
)
|
160
|
+
draw = ImageDraw.Draw(img)
|
161
|
+
draw.text((title_x, title_y), title, font=font, fill=(255, 255, 255))
|
162
|
+
if content:
|
163
|
+
draw.text(
|
164
|
+
(content_x, content_y),
|
165
|
+
content,
|
166
|
+
font=font,
|
167
|
+
fill=(255, 255, 255),
|
168
|
+
spacing=line_spacing,
|
169
|
+
)
|
170
|
+
return img
|
171
|
+
except Exception as e:
|
172
|
+
logger.error(f"绘制图片时出错: {e}")
|
173
|
+
error_img = Image.new("RGB", (600, 400), color=(255, 0, 0))
|
174
|
+
error_draw = ImageDraw.Draw(error_img)
|
175
|
+
error_draw.text((10, 10), "图片生成失败", fill=(255, 255, 255))
|
176
|
+
return error_img
|
177
|
+
|
178
|
+
player_info = await format_player_info(one_player)
|
179
|
+
server_message = build_server_message(one_server, player_info)
|
180
|
+
|
181
|
+
if is_img:
|
182
|
+
# 加载字体
|
183
|
+
font_path = Path(__file__).parent.parent / "data" / "font" / "loli.ttf"
|
184
|
+
font = ImageFont.truetype(str(font_path), 18)
|
185
|
+
draw = ImageDraw.Draw(Image.new("RGB", (600, 400), color=(73, 109, 137)))
|
186
|
+
img = draw_text_on_image(server_message, font, draw)
|
187
|
+
img_byte_arr = io.BytesIO()
|
188
|
+
img.save(img_byte_arr, format="PNG")
|
189
|
+
return img_byte_arr.getvalue()
|
190
|
+
return server_message
|
54
191
|
|
55
192
|
|
56
193
|
async def get_much_server(server_json: List[NserverOut], command: str):
|
@@ -64,7 +201,7 @@ async def get_much_server(server_json: List[NserverOut], command: str):
|
|
64
201
|
for index, i in enumerate(all_server):
|
65
202
|
out_server.append(
|
66
203
|
{
|
67
|
-
"server": i[0],
|
204
|
+
"server": i[0], # type: ignore
|
68
205
|
"player": i[1],
|
69
206
|
"host": server_json[index]["host"],
|
70
207
|
"port": server_json[index]["port"],
|
@@ -0,0 +1,176 @@
|
|
1
|
+
from typing import Dict, List, Optional, Tuple, Union, cast
|
2
|
+
|
3
|
+
from nonebot.log import logger
|
4
|
+
|
5
|
+
from ..l4_image import msg_to_image
|
6
|
+
from ..utils.api.models import NserverOut
|
7
|
+
from ..utils.api.request import L4API
|
8
|
+
from .draw_msg import convert_duration, draw_one_ip, get_much_server
|
9
|
+
|
10
|
+
|
11
|
+
def _get_server_json(
|
12
|
+
command: str,
|
13
|
+
allhost: Dict[str, List[NserverOut]],
|
14
|
+
) -> Optional[list]:
|
15
|
+
"""
|
16
|
+
根据命令获取服务器JSON列表
|
17
|
+
|
18
|
+
Args:
|
19
|
+
command (str): 服务器组名
|
20
|
+
ALLHOST (Dict): 全局服务器字典
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
Optional[list]: 服务器JSON列表,未找到组时返回None
|
24
|
+
"""
|
25
|
+
if command:
|
26
|
+
return allhost.get(command)
|
27
|
+
server_json = []
|
28
|
+
for servers in allhost.values():
|
29
|
+
server_json.extend(servers)
|
30
|
+
return server_json
|
31
|
+
|
32
|
+
|
33
|
+
async def _handle_group_info(
|
34
|
+
server_json: list,
|
35
|
+
command: str,
|
36
|
+
is_img: bool,
|
37
|
+
) -> Union[bytes, str, None]:
|
38
|
+
"""
|
39
|
+
处理服务器组信息请求
|
40
|
+
|
41
|
+
Args:
|
42
|
+
server_json (list): 服务器JSON列表
|
43
|
+
command (str): 服务器组名
|
44
|
+
is_img (bool): 是否返回图片格式
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Union[bytes, str, None]: 图片格式返回bytes,否则返回服务器列表
|
48
|
+
"""
|
49
|
+
logger.info(f"正在请求组服务器信息 {command}")
|
50
|
+
server_dict = await get_much_server(server_json, command)
|
51
|
+
if is_img:
|
52
|
+
return await msg_to_image(server_dict)
|
53
|
+
return str(server_dict)
|
54
|
+
|
55
|
+
|
56
|
+
async def get_single_server_info(
|
57
|
+
server_json: list,
|
58
|
+
_id: str,
|
59
|
+
) -> Optional[Tuple[str, int]]:
|
60
|
+
"""
|
61
|
+
获取单个服务器的host和port信息
|
62
|
+
|
63
|
+
Args:
|
64
|
+
server_json (list): 服务器JSON列表
|
65
|
+
_id (str): 服务器ID
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
Optional[Tuple[str, int]]: 返回(host, port)元组,未找到返回None
|
69
|
+
"""
|
70
|
+
logger.info("正在获取单服务器信息")
|
71
|
+
for i in server_json:
|
72
|
+
if str(_id) == str(i["id"]):
|
73
|
+
return i["host"], i["port"]
|
74
|
+
return None
|
75
|
+
|
76
|
+
|
77
|
+
async def _handle_single_server(
|
78
|
+
server_json: list,
|
79
|
+
_id: str,
|
80
|
+
is_img: bool,
|
81
|
+
) -> Union[bytes, str, None]:
|
82
|
+
"""
|
83
|
+
处理单个服务器信息请求
|
84
|
+
|
85
|
+
Args:
|
86
|
+
server_json (list): 服务器JSON列表
|
87
|
+
_id (str): 服务器ID
|
88
|
+
is_img (bool): 是否返回图片格式
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
Union[bytes, str, None]: 找到服务器时返回信息,否则返回None
|
92
|
+
"""
|
93
|
+
server_info = await get_single_server_info(server_json, _id)
|
94
|
+
if server_info is None:
|
95
|
+
return None
|
96
|
+
|
97
|
+
host, port = server_info
|
98
|
+
out_msg = await draw_one_ip(host, port)
|
99
|
+
if is_img:
|
100
|
+
return cast(bytes, out_msg)
|
101
|
+
return out_msg
|
102
|
+
|
103
|
+
|
104
|
+
async def _filter_servers(
|
105
|
+
servers: list,
|
106
|
+
tj_mode: str,
|
107
|
+
map_type: str = "普通药役",
|
108
|
+
) -> list:
|
109
|
+
"""筛选符合条件的服务器
|
110
|
+
Args:
|
111
|
+
servers: 服务器列表
|
112
|
+
tj_mode: 筛选模式('tj'或'zl')
|
113
|
+
map_type: 地图类型筛选条件
|
114
|
+
Returns:
|
115
|
+
符合条件的服务器列表
|
116
|
+
"""
|
117
|
+
filtered = []
|
118
|
+
for i in servers:
|
119
|
+
ser_list = await L4API.a2s_info([(i["host"], i["port"])], is_player=True)
|
120
|
+
if not ser_list:
|
121
|
+
continue
|
122
|
+
|
123
|
+
srv = ser_list[0][0]
|
124
|
+
players = ser_list[0][1]
|
125
|
+
|
126
|
+
if tj_mode == "tj" and map_type in srv.map_name:
|
127
|
+
score = sum(p.score for p in players[:4])
|
128
|
+
t = srv.map_name.split("[")[-1].split("特")[0]
|
129
|
+
if t.isdigit() and int(t) * 50 < score:
|
130
|
+
logger.info(
|
131
|
+
f"符合TJ条件的服务器: {i['host']}:{i['port']}, 地图: {srv.map_name}, 分数: {score}",
|
132
|
+
)
|
133
|
+
filtered.append(i)
|
134
|
+
elif tj_mode == "zl" and map_type in srv.map_name and len(players) <= 4:
|
135
|
+
logger.info(
|
136
|
+
f"符合ZL条件的服务器: {i['host']}:{i['port']}, 地图: {srv.map_name}, 玩家数: {len(players)}",
|
137
|
+
)
|
138
|
+
filtered.append(i)
|
139
|
+
return filtered
|
140
|
+
|
141
|
+
|
142
|
+
async def _format_players(player_list: list) -> str:
|
143
|
+
"""格式化玩家信息
|
144
|
+
Args:
|
145
|
+
player_list: 玩家对象列表
|
146
|
+
Returns:
|
147
|
+
格式化后的玩家信息字符串
|
148
|
+
"""
|
149
|
+
durations = [await convert_duration(p.duration) for p in player_list]
|
150
|
+
max_duration_len = max(len(str(d)) for d in durations)
|
151
|
+
max_score_len = max(len(str(p.score)) for p in player_list)
|
152
|
+
return "\n".join(
|
153
|
+
f"[{p.score:>{max_score_len}}] | {durations[i]:^{max_duration_len}} | {p.name[0]}***{p.name[-1]}"
|
154
|
+
for i, p in enumerate(player_list)
|
155
|
+
)
|
156
|
+
|
157
|
+
|
158
|
+
def _build_message(srv_info, players_msg: str, selected_srv: dict, config) -> str:
|
159
|
+
"""构建服务器信息消息
|
160
|
+
Args:
|
161
|
+
srv_info: 服务器信息对象
|
162
|
+
players_msg: 格式化后的玩家信息
|
163
|
+
selected_srv: 选中的服务器信息
|
164
|
+
config: 配置对象
|
165
|
+
Returns:
|
166
|
+
完整的消息字符串
|
167
|
+
"""
|
168
|
+
msg = f"""*{srv_info.server_name}*
|
169
|
+
游戏: {srv_info.folder}
|
170
|
+
地图: {srv_info.map_name}
|
171
|
+
人数: {srv_info.player_count}/{srv_info.max_players}"""
|
172
|
+
if srv_info.ping is not None:
|
173
|
+
msg += f"\nping: {srv_info.ping * 1000:.0f}ms\n{players_msg}"
|
174
|
+
if config.l4_show_ip:
|
175
|
+
msg += f"\nconnect {selected_srv['host']}:{selected_srv['port']}"
|
176
|
+
return msg
|
@@ -3,18 +3,17 @@ import contextlib
|
|
3
3
|
import socket
|
4
4
|
from copy import deepcopy
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Any, Dict, List, Literal, Optional, Tuple, cast
|
6
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
7
7
|
|
8
8
|
import a2s
|
9
9
|
import aiofiles
|
10
10
|
import ujson as js
|
11
11
|
import ujson as json
|
12
|
-
from bs4 import BeautifulSoup
|
12
|
+
from bs4 import BeautifulSoup, Tag
|
13
13
|
from httpx import AsyncClient
|
14
|
+
from nonebot.log import logger
|
14
15
|
|
15
16
|
from ...config import config
|
16
|
-
|
17
|
-
# from nonebot.log import logger
|
18
17
|
from ..utils import split_maohao
|
19
18
|
from .api import AnnePlayerApi, AnneSearchApi, anne_ban
|
20
19
|
from .models import (
|
@@ -42,35 +41,57 @@ class L4D2Api:
|
|
42
41
|
"Content-Type": "application/x-www-form-urlencoded",
|
43
42
|
}
|
44
43
|
|
44
|
+
def safe_select(self, element: Optional[Tag], selector: str) -> List[Any]:
|
45
|
+
"""安全地调用 select 方法"""
|
46
|
+
if isinstance(element, Tag):
|
47
|
+
return element.select(selector)
|
48
|
+
return []
|
49
|
+
|
50
|
+
def safe_find_all(
|
51
|
+
self,
|
52
|
+
element: Optional[Tag],
|
53
|
+
tag: str,
|
54
|
+
class_: str = "",
|
55
|
+
) -> List[Any]:
|
56
|
+
"""安全地调用 find_all 方法"""
|
57
|
+
if isinstance(element, Tag):
|
58
|
+
return element.find_all(tag, class_=class_)
|
59
|
+
return []
|
60
|
+
|
45
61
|
async def a2s_info(
|
46
62
|
self,
|
47
63
|
ip_list: List[Tuple[str, int]],
|
48
64
|
is_server: bool = True,
|
49
65
|
is_player: bool = False,
|
50
|
-
) -> List[
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
),
|
65
|
-
),
|
66
|
-
)
|
67
|
-
except ValueError:
|
68
|
-
continue # 处理异常情况
|
66
|
+
) -> List[
|
67
|
+
Tuple[Union[a2s.SourceInfo[str], a2s.GoldSrcInfo[str]], List[a2s.Player]]
|
68
|
+
]:
|
69
|
+
msg_list: List[
|
70
|
+
Tuple[Union[a2s.SourceInfo[str], a2s.GoldSrcInfo[str]], List[a2s.Player]]
|
71
|
+
] = []
|
72
|
+
|
73
|
+
if ip_list:
|
74
|
+
tasks = [
|
75
|
+
asyncio.create_task(
|
76
|
+
self.process_message(ip, index, is_server, is_player),
|
77
|
+
)
|
78
|
+
for index, ip in enumerate(ip_list)
|
79
|
+
]
|
69
80
|
|
70
|
-
|
71
|
-
|
81
|
+
try:
|
82
|
+
results = await asyncio.gather(*tasks)
|
83
|
+
msg_list = [r for r in results if r is not None]
|
84
|
+
except Exception as e:
|
85
|
+
logger.error(f"获取服务器信息时发生错误: {e}")
|
72
86
|
|
73
|
-
|
87
|
+
# 使用稳定的排序方式,避免服务器频繁变动位置
|
88
|
+
return sorted(
|
89
|
+
msg_list,
|
90
|
+
key=lambda x: (
|
91
|
+
getattr(x[0], "steam_id", float("inf")) is None,
|
92
|
+
getattr(x[0], "steam_id", float("inf")),
|
93
|
+
),
|
94
|
+
)
|
74
95
|
|
75
96
|
async def process_message(
|
76
97
|
self,
|
@@ -80,19 +101,24 @@ class L4D2Api:
|
|
80
101
|
is_player: bool,
|
81
102
|
):
|
82
103
|
play: List[a2s.Player] = []
|
104
|
+
server: Union[a2s.SourceInfo, a2s.GoldSrcInfo]
|
83
105
|
if is_server:
|
84
106
|
try:
|
85
|
-
server
|
107
|
+
server = await a2s.ainfo(
|
108
|
+
ip,
|
109
|
+
timeout=3,
|
110
|
+
encoding="utf8",
|
111
|
+
)
|
86
112
|
|
87
113
|
if server is not None:
|
88
|
-
server.steam_id = index
|
114
|
+
server.steam_id = index # type: ignore
|
89
115
|
|
90
116
|
except (
|
91
117
|
asyncio.exceptions.TimeoutError,
|
92
118
|
ConnectionRefusedError,
|
93
119
|
socket.gaierror,
|
94
120
|
):
|
95
|
-
server
|
121
|
+
server = a2s.SourceInfo(
|
96
122
|
protocol=0,
|
97
123
|
server_name="服务器无响应",
|
98
124
|
map_name="无",
|
@@ -137,7 +163,7 @@ class L4D2Api:
|
|
137
163
|
json: Optional[Dict[str, Any]] = None,
|
138
164
|
data: Optional[Dict[str, Any]] = None,
|
139
165
|
is_json: bool = True,
|
140
|
-
):
|
166
|
+
) -> Union[Dict[str, Any], BeautifulSoup]: # type: ignore
|
141
167
|
header = deepcopy(self._HEADER)
|
142
168
|
|
143
169
|
if json is not None:
|
@@ -181,7 +207,17 @@ class L4D2Api:
|
|
181
207
|
return BeautifulSoup(html_content, "lxml")
|
182
208
|
|
183
209
|
async def get_sourceban(self, tag: str = "云", url: str = anne_ban):
|
184
|
-
"""
|
210
|
+
"""
|
211
|
+
异步函数,从sourceban++获取服务器列表,目前未做名称处理。
|
212
|
+
|
213
|
+
Args:
|
214
|
+
tag (str): 用于标识不同来源的标签,默认为"云"。
|
215
|
+
url (str): SourceBan的URL,默认为anne_ban。
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
list: 包含服务器信息的列表。
|
219
|
+
|
220
|
+
"""
|
185
221
|
if not (url.startswith(("http://", "https://"))):
|
186
222
|
url = "http://" + url # 默认添加 http://
|
187
223
|
soup = await self._server_request(
|
@@ -216,14 +252,12 @@ class L4D2Api:
|
|
216
252
|
tag_path = Path(Path(config.l4_path) / f"l4d2/{tag}.json")
|
217
253
|
|
218
254
|
async with aiofiles.open(tag_path, "w", encoding="utf-8") as f:
|
219
|
-
print(Path(Path(config.l4_path) / f"l4d2/{tag}.json"))
|
220
255
|
up_data = {}
|
221
256
|
for server in server_list:
|
222
257
|
new_dict = {}
|
223
258
|
new_dict["id"] = int(server.index) + 1
|
224
259
|
new_dict["ip"] = server.host + ":" + str(server.port)
|
225
260
|
up_data.update(new_dict)
|
226
|
-
print(up_data)
|
227
261
|
json.dump(up_data, f, ensure_ascii=False, indent=4)
|
228
262
|
return server_list
|
229
263
|
|
@@ -258,7 +292,7 @@ class L4D2Api:
|
|
258
292
|
"last_time": td_tags,
|
259
293
|
},
|
260
294
|
)
|
261
|
-
|
295
|
+
logger.debug(server_list)
|
262
296
|
return cast(List[AnneSearch], server_list)
|
263
297
|
|
264
298
|
async def get_anne_playerdetail(self, steamid: str):
|
@@ -272,14 +306,14 @@ class L4D2Api:
|
|
272
306
|
if not isinstance(soup, BeautifulSoup):
|
273
307
|
return None
|
274
308
|
|
275
|
-
tbody = soup.find(
|
309
|
+
tbody = cast(BeautifulSoup, soup).find(
|
276
310
|
"div",
|
277
311
|
class_="content text-center text-md-left",
|
278
312
|
style="background-color: #f2f2f2;",
|
279
313
|
)
|
280
314
|
if tbody is None:
|
281
315
|
return None
|
282
|
-
kill_tag = tbody.find(
|
316
|
+
kill_tag = cast(BeautifulSoup, tbody).find(
|
283
317
|
"div",
|
284
318
|
class_="card-body worldmap d-flex flex-column justify-content-center text-center",
|
285
319
|
)
|
@@ -288,7 +322,7 @@ class L4D2Api:
|
|
288
322
|
"table",
|
289
323
|
class_="table content-table-noborder text-left",
|
290
324
|
)
|
291
|
-
|
325
|
+
|
292
326
|
info_tag = tbody_tags[0]
|
293
327
|
detail_tag = tbody_tags[1]
|
294
328
|
error_tag = tbody_tags[2]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import Union
|
2
|
+
|
3
|
+
from nonebot_plugin_alconna import UniMessage
|
4
|
+
|
5
|
+
|
6
|
+
async def out_msg_out(
|
7
|
+
msg: Union[str, bytes, UniMessage],
|
8
|
+
is_connect: bool = False,
|
9
|
+
host: str = "",
|
10
|
+
port: str = "",
|
11
|
+
):
|
12
|
+
"""
|
13
|
+
统一消息输出函数
|
14
|
+
|
15
|
+
Args:
|
16
|
+
msg: 要输出的消息内容
|
17
|
+
is_connect: 是否为连接消息
|
18
|
+
host: 服务器地址
|
19
|
+
port: 服务器端口
|
20
|
+
"""
|
21
|
+
if isinstance(msg, UniMessage):
|
22
|
+
return await msg.finish()
|
23
|
+
if isinstance(msg, str):
|
24
|
+
await UniMessage.text(msg).finish()
|
25
|
+
if is_connect:
|
26
|
+
out = UniMessage.image(raw=msg) + UniMessage.text(
|
27
|
+
f"连接到服务器: {host}:{port}",
|
28
|
+
)
|
29
|
+
return await out.finish()
|
30
|
+
return await UniMessage.image(raw=msg).finish()
|
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
7
7
|
import aiofiles
|
8
8
|
import aiohttp
|
9
9
|
import nonebot
|
10
|
+
from aiohttp import ClientTimeout
|
10
11
|
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
|
11
12
|
from nonebot.log import logger
|
12
13
|
from nonebot_plugin_alconna import UniMessage
|
@@ -184,7 +185,11 @@ async def url_to_byte(url: str):
|
|
184
185
|
"""获取URL数据的字节流"""
|
185
186
|
|
186
187
|
async with aiohttp.ClientSession() as session:
|
187
|
-
async with session.get(
|
188
|
+
async with session.get(
|
189
|
+
url,
|
190
|
+
headers=headers,
|
191
|
+
timeout=ClientTimeout(total=600),
|
192
|
+
) as response:
|
188
193
|
if response.status == 200:
|
189
194
|
return await response.read()
|
190
195
|
return None
|
@@ -194,7 +199,11 @@ async def url_to_msg(url: str):
|
|
194
199
|
"""获取URL数据的字节流"""
|
195
200
|
|
196
201
|
async with aiohttp.ClientSession() as session:
|
197
|
-
async with session.get(
|
202
|
+
async with session.get(
|
203
|
+
url,
|
204
|
+
headers=headers,
|
205
|
+
timeout=ClientTimeout(total=600),
|
206
|
+
) as response:
|
198
207
|
if response.status == 200:
|
199
208
|
return await response.text()
|
200
209
|
return None
|