koishi-plugin-steam-info-check 1.0.9 → 1.1.0
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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +109 -40
- package/dist/locales/zh-CN.d.ts +25 -0
- package/dist/locales/zh-CN.js +25 -0
- package/nonebot-plugin-steam-info-main/.github/actions/setup-python/action.yml +21 -0
- package/nonebot-plugin-steam-info-main/.github/workflows/release.yml +37 -0
- package/nonebot-plugin-steam-info-main/LICENSE +21 -0
- package/nonebot-plugin-steam-info-main/README.md +117 -0
- package/nonebot-plugin-steam-info-main/fonts/MiSans-Bold.ttf +0 -0
- package/nonebot-plugin-steam-info-main/fonts/MiSans-Light.ttf +0 -0
- package/nonebot-plugin-steam-info-main/fonts/MiSans-Regular.ttf +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/__init__.py +487 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/config.py +19 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/data_source.py +264 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/draw.py +921 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/models.py +82 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/bg_dots.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/busy.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/default_achievement_image.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/default_header_image.jpg +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/friends_search.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/gaming.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/parent_status.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/unknown_avatar.jpg +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/zzz_gaming.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/zzz_online.png +0 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/steam.py +259 -0
- package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/utils.py +112 -0
- package/nonebot-plugin-steam-info-main/pdm.lock +966 -0
- package/nonebot-plugin-steam-info-main/preview.png +0 -0
- package/nonebot-plugin-steam-info-main/preview_1.png +0 -0
- package/nonebot-plugin-steam-info-main/preview_2.png +0 -0
- package/nonebot-plugin-steam-info-main/pyproject.toml +29 -0
- package/package.json +1 -1
- package/src/index.ts +83 -42
- package/src/locales/zh-CN.ts +25 -0
- package/src/locales/zh-CN.yml +2 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import TypedDict, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Player(TypedDict):
|
|
5
|
+
steamid: str
|
|
6
|
+
communityvisibilitystate: int
|
|
7
|
+
profilestate: int
|
|
8
|
+
personaname: str
|
|
9
|
+
profileurl: str
|
|
10
|
+
avatar: str
|
|
11
|
+
avatarmedium: str
|
|
12
|
+
avatarfull: str
|
|
13
|
+
avatarhash: str
|
|
14
|
+
lastlogoff: int
|
|
15
|
+
personastate: int
|
|
16
|
+
realname: str
|
|
17
|
+
primaryclanid: str
|
|
18
|
+
timecreated: int
|
|
19
|
+
personastateflags: int
|
|
20
|
+
# gameextrainfo: str
|
|
21
|
+
# gameid: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlayerSummariesResponse(TypedDict):
|
|
25
|
+
players: List[Player]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PlayerSummaries(TypedDict):
|
|
29
|
+
response: PlayerSummariesResponse
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProcessedPlayer(Player):
|
|
33
|
+
game_start_time: int # Unix timestamp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PlayerSummariesProcessedResponse(TypedDict):
|
|
37
|
+
players: List[ProcessedPlayer]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Achievements(TypedDict):
|
|
41
|
+
name: str
|
|
42
|
+
image: bytes
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GameData(TypedDict):
|
|
46
|
+
game_name: str
|
|
47
|
+
play_time: str # e.g. 10.2
|
|
48
|
+
last_played: str # e.g. 10 月 2 日
|
|
49
|
+
game_image: bytes
|
|
50
|
+
achievements: List[Achievements]
|
|
51
|
+
completed_achievement_number: int
|
|
52
|
+
total_achievement_number: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PlayerData(TypedDict):
|
|
56
|
+
steamid: str
|
|
57
|
+
player_name: str
|
|
58
|
+
background: bytes
|
|
59
|
+
avatar: bytes
|
|
60
|
+
description: str
|
|
61
|
+
recent_2_week_play_time: str
|
|
62
|
+
game_data: List[GameData]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DrawPlayerStatusData(TypedDict):
|
|
66
|
+
game_name: str
|
|
67
|
+
game_time: str # e.g. 10.2 小时(过去 2 周)
|
|
68
|
+
last_play_time: str # e.g. 10 月 2 日
|
|
69
|
+
game_header: bytes
|
|
70
|
+
achievements: List[Achievements]
|
|
71
|
+
completed_achievement_number: int
|
|
72
|
+
total_achievement_number: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"Player",
|
|
77
|
+
"PlayerSummaries",
|
|
78
|
+
"PlayerSummariesResponse",
|
|
79
|
+
"ProcessedPlayer",
|
|
80
|
+
"PlayerSummariesProcessedResponse",
|
|
81
|
+
"DrawPlayerStatusData",
|
|
82
|
+
]
|
package/nonebot-plugin-steam-info-main/nonebot_plugin_steam_info/res/default_achievement_image.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import httpx
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from nonebot.log import logger
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
from .models import PlayerSummaries, PlayerData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
STEAM_ID_OFFSET = 76561197960265728
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_steam_id(steam_id_or_steam_friends_code: str) -> str:
|
|
16
|
+
if not steam_id_or_steam_friends_code.isdigit():
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
id_ = int(steam_id_or_steam_friends_code)
|
|
20
|
+
|
|
21
|
+
if id_ < STEAM_ID_OFFSET:
|
|
22
|
+
return str(id_ + STEAM_ID_OFFSET)
|
|
23
|
+
|
|
24
|
+
return steam_id_or_steam_friends_code
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def get_steam_users_info(
|
|
28
|
+
steam_ids: List[str], steam_api_key: List[str], proxy: str = None
|
|
29
|
+
) -> PlayerSummaries:
|
|
30
|
+
if len(steam_ids) == 0:
|
|
31
|
+
return {"response": {"players": []}}
|
|
32
|
+
|
|
33
|
+
if len(steam_ids) > 100:
|
|
34
|
+
# 分批获取
|
|
35
|
+
result = {"response": {"players": []}}
|
|
36
|
+
for i in range(0, len(steam_ids), 100):
|
|
37
|
+
batch_result = await get_steam_users_info(
|
|
38
|
+
steam_ids[i : i + 100], steam_api_key, proxy
|
|
39
|
+
)
|
|
40
|
+
result["response"]["players"].extend(batch_result["response"]["players"])
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
for api_key in steam_api_key:
|
|
44
|
+
try:
|
|
45
|
+
async with httpx.AsyncClient(proxy=proxy) as client:
|
|
46
|
+
response = await client.get(
|
|
47
|
+
f'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={api_key}&steamids={",".join(steam_ids)}'
|
|
48
|
+
)
|
|
49
|
+
if response.status_code == 200:
|
|
50
|
+
return response.json()
|
|
51
|
+
else:
|
|
52
|
+
logger.warning(f"API key {api_key} failed to get steam users info.")
|
|
53
|
+
except httpx.RequestError as exc:
|
|
54
|
+
logger.warning(f"API key {api_key} encountered an error: {exc}")
|
|
55
|
+
|
|
56
|
+
logger.error("All API keys failed to get steam users info.")
|
|
57
|
+
return {"response": {"players": []}}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _fetch(
|
|
61
|
+
url: str, default: bytes, cache_file: Optional[Path] = None, proxy: str = None
|
|
62
|
+
) -> bytes:
|
|
63
|
+
if cache_file is not None and cache_file.exists():
|
|
64
|
+
return cache_file.read_bytes()
|
|
65
|
+
try:
|
|
66
|
+
async with httpx.AsyncClient(proxy=proxy) as client:
|
|
67
|
+
response = await client.get(url)
|
|
68
|
+
if response.status_code == 200:
|
|
69
|
+
if cache_file is not None:
|
|
70
|
+
cache_file.write_bytes(response.content)
|
|
71
|
+
return response.content
|
|
72
|
+
else:
|
|
73
|
+
response.raise_for_status()
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.error(f"Failed to get image: {exc}")
|
|
76
|
+
return default
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def get_user_data(
|
|
80
|
+
steam_id: int, cache_path: Path, proxy: str = None
|
|
81
|
+
) -> PlayerData:
|
|
82
|
+
url = f"https://steamcommunity.com/profiles/{steam_id}"
|
|
83
|
+
default_background = (Path(__file__).parent / "res/bg_dots.png").read_bytes()
|
|
84
|
+
default_avatar = (Path(__file__).parent / "res/unknown_avatar.jpg").read_bytes()
|
|
85
|
+
default_achievement_image = (
|
|
86
|
+
Path(__file__).parent / "res/default_achievement_image.png"
|
|
87
|
+
).read_bytes()
|
|
88
|
+
default_header_image = (
|
|
89
|
+
Path(__file__).parent / "res/default_header_image.jpg"
|
|
90
|
+
).read_bytes()
|
|
91
|
+
|
|
92
|
+
result = {
|
|
93
|
+
"description": "No information given.",
|
|
94
|
+
"background": default_background,
|
|
95
|
+
"avatar": default_avatar,
|
|
96
|
+
"player_name": "Unknown",
|
|
97
|
+
"recent_2_week_play_time": None,
|
|
98
|
+
"game_data": [],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
local_time = datetime.now(timezone.utc).astimezone()
|
|
102
|
+
utc_offset_minutes = int(local_time.utcoffset().total_seconds())
|
|
103
|
+
timezone_cookie_value = f"{utc_offset_minutes},0"
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
async with httpx.AsyncClient(
|
|
107
|
+
proxy=proxy,
|
|
108
|
+
headers={
|
|
109
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"
|
|
110
|
+
},
|
|
111
|
+
cookies={"timezoneOffset": timezone_cookie_value},
|
|
112
|
+
) as client:
|
|
113
|
+
response = await client.get(url)
|
|
114
|
+
if response.status_code == 200:
|
|
115
|
+
html = response.text
|
|
116
|
+
elif response.status_code == 302:
|
|
117
|
+
url = response.headers["Location"]
|
|
118
|
+
response = await client.get(url)
|
|
119
|
+
if response.status_code == 200:
|
|
120
|
+
html = response.text
|
|
121
|
+
else:
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
except httpx.RequestError as exc:
|
|
124
|
+
logger.error(f"Failed to get user data: {exc}")
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
# player name
|
|
128
|
+
player_name = re.search(r"<title>Steam 社区 :: (.*?)</title>", html)
|
|
129
|
+
if player_name:
|
|
130
|
+
result["player_name"] = player_name.group(1)
|
|
131
|
+
|
|
132
|
+
# description t<div class="profile_summary">\r\n\t\t\t\t\t\t\t\t風が雨が激しくても<br>思いだすんだ 僕らを照らす光があるよ<br>今日もいっぱい<br>明日もいっぱい 力を出しきってみるよ\t\t\t\t\t\t\t</div>
|
|
133
|
+
description = re.search(
|
|
134
|
+
r'<div class="profile_summary">(.*?)</div>', html, re.DOTALL | re.MULTILINE
|
|
135
|
+
)
|
|
136
|
+
if description:
|
|
137
|
+
description = description.group(1)
|
|
138
|
+
description = re.sub(r"<br>", "\n", description)
|
|
139
|
+
description = re.sub(r"\t", "", description)
|
|
140
|
+
result["description"] = description.strip()
|
|
141
|
+
|
|
142
|
+
# remove emoji
|
|
143
|
+
result["description"] = re.sub(r"ː.*?ː", "", result["description"])
|
|
144
|
+
|
|
145
|
+
# remove xml
|
|
146
|
+
result["description"] = re.sub(r"<.*?>", "", result["description"])
|
|
147
|
+
|
|
148
|
+
# background
|
|
149
|
+
background_url = re.search(r"background-image: url\( \'(.*?)\' \)", html)
|
|
150
|
+
if background_url:
|
|
151
|
+
background_url = background_url.group(1)
|
|
152
|
+
result["background"] = await _fetch(
|
|
153
|
+
background_url, default_background, proxy=proxy
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# avatar
|
|
157
|
+
# \t<link rel="image_src" href="https://avatars.akamai.steamstatic.com/3ade30f61c3d2cc0b8c80aaf567b573cd022c405_full.jpg">
|
|
158
|
+
avatar_url = re.search(r'<link rel="image_src" href="(.*?)"', html)
|
|
159
|
+
if avatar_url:
|
|
160
|
+
avatar_url = avatar_url.group(1)
|
|
161
|
+
# https://avatars.akamai.steamstatic.com/3ade30f61c3d2cc0b8c80aaf567b573cd022c405_full.jpg
|
|
162
|
+
avatar_url_split = avatar_url.split("/")
|
|
163
|
+
avatar_file = cache_path / f"avatar_{avatar_url_split[-1].split('_')[0]}.jpg"
|
|
164
|
+
result["avatar"] = await _fetch(
|
|
165
|
+
avatar_url, default_avatar, cache_file=avatar_file, proxy=proxy
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# recent 2 week play time
|
|
169
|
+
# \t<div class="recentgame_quicklinks recentgame_recentplaytime">\r\n\t\t\t\t\t\t\t\t\t<div>15.5 小时(过去 2 周)</div>
|
|
170
|
+
play_time_text = re.search(
|
|
171
|
+
r'<div class="recentgame_quicklinks recentgame_recentplaytime">\s*<div>(.*?)</div>',
|
|
172
|
+
html,
|
|
173
|
+
)
|
|
174
|
+
if play_time_text:
|
|
175
|
+
play_time_text = play_time_text.group(1)
|
|
176
|
+
result["recent_2_week_play_time"] = play_time_text
|
|
177
|
+
|
|
178
|
+
# game data
|
|
179
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
180
|
+
game_data = []
|
|
181
|
+
recent_games = soup.find_all("div", class_="recent_game")
|
|
182
|
+
|
|
183
|
+
for game in recent_games:
|
|
184
|
+
game_info = {}
|
|
185
|
+
game_info["game_name"] = game.find("div", class_="game_name").text.strip()
|
|
186
|
+
game_info["game_image_url"] = game.find("img", class_="game_capsule")["src"]
|
|
187
|
+
game_info_split = game_info["game_image_url"].split("/")
|
|
188
|
+
# https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1144400/capsule_184x69_schinese.jpg?t=1724440433
|
|
189
|
+
|
|
190
|
+
game_info["game_image"] = await _fetch(
|
|
191
|
+
game_info["game_image_url"],
|
|
192
|
+
default_header_image,
|
|
193
|
+
cache_file=cache_path / f"header_{game_info_split[-2]}.jpg",
|
|
194
|
+
proxy=proxy,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
play_time_text = game.find("div", class_="game_info_details").text.strip()
|
|
198
|
+
play_time = re.search(r"总时数\s*(.*?)\s*小时", play_time_text)
|
|
199
|
+
if play_time is None:
|
|
200
|
+
game_info["play_time"] = ""
|
|
201
|
+
else:
|
|
202
|
+
game_info["play_time"] = play_time.group(1)
|
|
203
|
+
|
|
204
|
+
last_played = re.search(r"最后运行日期:(.*) 日", play_time_text)
|
|
205
|
+
if last_played is not None:
|
|
206
|
+
game_info["last_played"] = "最后运行日期:" + last_played.group(1) + " 日"
|
|
207
|
+
else:
|
|
208
|
+
game_info["last_played"] = "当前正在游戏"
|
|
209
|
+
achievements = []
|
|
210
|
+
achievement_elements = game.find_all("div", class_="game_info_achievement")
|
|
211
|
+
for achievement in achievement_elements:
|
|
212
|
+
if "plus_more" in achievement["class"]:
|
|
213
|
+
continue
|
|
214
|
+
achievement_info = {}
|
|
215
|
+
achievement_info["name"] = achievement["data-tooltip-text"]
|
|
216
|
+
achievement_info["image_url"] = achievement.find("img")["src"]
|
|
217
|
+
achievement_info_split = achievement_info["image_url"].split("/")
|
|
218
|
+
|
|
219
|
+
achievement_info["image"] = await _fetch(
|
|
220
|
+
achievement_info["image_url"],
|
|
221
|
+
default_achievement_image,
|
|
222
|
+
cache_file=cache_path
|
|
223
|
+
/ f"achievement_{achievement_info_split[-2]}_{achievement_info_split[-1]}",
|
|
224
|
+
proxy=proxy,
|
|
225
|
+
)
|
|
226
|
+
achievements.append(achievement_info)
|
|
227
|
+
game_info["achievements"] = achievements
|
|
228
|
+
game_info_achievement_summary = game.find(
|
|
229
|
+
"span", class_="game_info_achievement_summary"
|
|
230
|
+
)
|
|
231
|
+
if game_info_achievement_summary is None:
|
|
232
|
+
game_data.append(game_info)
|
|
233
|
+
continue
|
|
234
|
+
remain_achievement_text = game_info_achievement_summary.find(
|
|
235
|
+
"span", class_="ellipsis"
|
|
236
|
+
).text
|
|
237
|
+
game_info["completed_achievement_number"] = int(
|
|
238
|
+
remain_achievement_text.split("/")[0].strip()
|
|
239
|
+
)
|
|
240
|
+
game_info["total_achievement_number"] = int(
|
|
241
|
+
remain_achievement_text.split("/")[1].strip()
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
game_data.append(game_info)
|
|
245
|
+
|
|
246
|
+
result["game_data"] = game_data
|
|
247
|
+
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
from nonebot.log import logger
|
|
253
|
+
import asyncio
|
|
254
|
+
|
|
255
|
+
data = asyncio.run(get_user_data(76561199135038179, None))
|
|
256
|
+
|
|
257
|
+
with open("bg.jpg", "wb") as f:
|
|
258
|
+
f.write(data["background"])
|
|
259
|
+
logger.info(data["description"])
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import pytz
|
|
3
|
+
import httpx
|
|
4
|
+
import datetime
|
|
5
|
+
import calendar
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
from .models import Player
|
|
12
|
+
from .data_source import BindData
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def _fetch_avatar(avatar_url: str, proxy: str = None) -> Image.Image:
|
|
16
|
+
async with httpx.AsyncClient(proxy=proxy) as client:
|
|
17
|
+
response = await client.get(avatar_url)
|
|
18
|
+
if response.status_code != 200:
|
|
19
|
+
return Image.open(Path(__file__).parent / "res/unknown_avatar.jpg")
|
|
20
|
+
return Image.open(BytesIO(response.content))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def fetch_avatar(
|
|
24
|
+
player: Player, avatar_dir: Optional[Path], proxy: str = None
|
|
25
|
+
) -> Image.Image:
|
|
26
|
+
if avatar_dir is not None:
|
|
27
|
+
avatar_path = (
|
|
28
|
+
avatar_dir / f"avatar_{player['steamid']}_{player['avatarhash']}.png"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if avatar_path.exists():
|
|
32
|
+
avatar = Image.open(avatar_path)
|
|
33
|
+
else:
|
|
34
|
+
avatar = await _fetch_avatar(player["avatarfull"], proxy)
|
|
35
|
+
|
|
36
|
+
avatar.save(avatar_path)
|
|
37
|
+
else:
|
|
38
|
+
avatar = await _fetch_avatar(player["avatarfull"], proxy)
|
|
39
|
+
|
|
40
|
+
return avatar
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def convert_player_name_to_nickname(
|
|
44
|
+
data: Dict[str, str], parent_id: str, bind_data: BindData
|
|
45
|
+
) -> Dict[str, str]:
|
|
46
|
+
data["nickname"] = bind_data.get_by_steam_id(parent_id, data["steamid"])["nickname"]
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def simplize_steam_player_data(
|
|
51
|
+
player: Player, proxy: str = None, avatar_dir: Path = None
|
|
52
|
+
) -> Dict[str, str]:
|
|
53
|
+
avatar = await fetch_avatar(player, avatar_dir, proxy)
|
|
54
|
+
|
|
55
|
+
if player["personastate"] == 0:
|
|
56
|
+
if not player.get("lastlogoff"):
|
|
57
|
+
status = "离线"
|
|
58
|
+
else:
|
|
59
|
+
time_logged_off = player["lastlogoff"] # Unix timestamp
|
|
60
|
+
time_to_now = calendar.timegm(time.gmtime()) - time_logged_off
|
|
61
|
+
|
|
62
|
+
# 将时间转换为自然语言
|
|
63
|
+
if time_to_now < 60:
|
|
64
|
+
status = "上次在线 刚刚"
|
|
65
|
+
elif time_to_now < 3600:
|
|
66
|
+
status = f"上次在线 {time_to_now // 60} 分钟前"
|
|
67
|
+
elif time_to_now < 86400:
|
|
68
|
+
status = f"上次在线 {time_to_now // 3600} 小时前"
|
|
69
|
+
elif time_to_now < 2592000:
|
|
70
|
+
status = f"上次在线 {time_to_now // 86400} 天前"
|
|
71
|
+
elif time_to_now < 31536000:
|
|
72
|
+
status = f"上次在线 {time_to_now // 2592000} 个月前"
|
|
73
|
+
else:
|
|
74
|
+
status = f"上次在线 {time_to_now // 31536000} 年前"
|
|
75
|
+
elif player["personastate"] in [1, 2, 4]:
|
|
76
|
+
status = (
|
|
77
|
+
"在线" if player.get("gameextrainfo") is None else player["gameextrainfo"]
|
|
78
|
+
)
|
|
79
|
+
elif player["personastate"] == 3:
|
|
80
|
+
status = (
|
|
81
|
+
"离开" if player.get("gameextrainfo") is None else player["gameextrainfo"]
|
|
82
|
+
)
|
|
83
|
+
elif player["personastate"] in [5, 6]:
|
|
84
|
+
status = "在线"
|
|
85
|
+
else:
|
|
86
|
+
status = "未知"
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"steamid": player["steamid"],
|
|
90
|
+
"avatar": avatar,
|
|
91
|
+
"name": player["personaname"],
|
|
92
|
+
"status": status,
|
|
93
|
+
"personastate": player["personastate"],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def image_to_bytes(image: Image.Image) -> bytes:
|
|
98
|
+
with BytesIO() as bio:
|
|
99
|
+
image.save(bio, format="PNG")
|
|
100
|
+
return bio.getvalue()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def hex_to_rgb(hex_color: str):
|
|
104
|
+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_timestamp_to_beijing_time(timestamp: int) -> str:
|
|
108
|
+
beijing_timezone = pytz.timezone("Asia/Shanghai")
|
|
109
|
+
date_utc = datetime.datetime.fromtimestamp(timestamp, pytz.utc)
|
|
110
|
+
date_beijing = date_utc.astimezone(beijing_timezone)
|
|
111
|
+
return date_beijing.strftime("%Y-%m-%d %H:%M:%S")
|
|
112
|
+
# example: 2021-09-06 21:00:00
|