feat(steam-info): 资料页状态/最近游玩/ban 描述增强,时长图渲染与定时任务迁移
- steam.py / draw.py / handlers 扩充:个人资料页状态构建、最近游玩、 游戏头像与中文游戏名查询、ban 描述 - 定时播报迁入 hexi.core.scheduler - 新增 tests/test_steam_profile_parse.py(资料 HTML/XML 解析) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
0ac1958932
commit
6af60a7d55
6 files changed
+1298
-125
No files matched your search
@@ -34,20 +34,31 @@ from .. import (
|
||||
get_steam_users_info,
|
||||
get_target,
|
||||
get_user_data,
|
||||
get_recently_played,
|
||||
fetch_game_header_image,
|
||||
image_to_bytes,
|
||||
key_select,
|
||||
localize_game_name,
|
||||
simplize_steam_player_data,
|
||||
build_profile_status,
|
||||
describe_ban,
|
||||
_fill_missing_icon_hashes,
|
||||
_query_game_names,
|
||||
_query_game_name_ch_by_en,
|
||||
_upsert_game_icon_hashes,
|
||||
update_steamdb_apps,
|
||||
)
|
||||
|
||||
# 信息卡「游戏动态」最多展示的条数(按近两周时长排序,列表本身最多 8 条)
|
||||
MAX_ACTIVITY_GAMES = 4
|
||||
|
||||
help = on_command("steamhelp", aliases={"steam帮助"}, priority=10)
|
||||
bind = on_command("steambind", aliases={"绑定steam"}, priority=10)
|
||||
unbind = on_command("steamunbind", aliases={"解绑steam"}, priority=10)
|
||||
info = on_command("steaminfo", aliases={"steam信息"}, priority=10)
|
||||
# steaminfo 会额外调一次 Steam API(近两周时长),按群冷却防止被刷配额
|
||||
info = cooldown(30, scope="group", hint="群里刚有人查过,{secs} 秒后再试")(
|
||||
on_command("steaminfo", aliases={"steam信息"}, priority=10)
|
||||
)
|
||||
check = cooldown(30, scope="group", hint="群里刚有人查过,{secs} 秒后再试")(
|
||||
on_command("steamcheck", aliases={"查看steam", "查steam", "谁在玩游戏"}, priority=10)
|
||||
)
|
||||
@@ -128,29 +139,118 @@ async def info_handle(bot: Bot, event: Event, target: Target = Depends(get_targe
|
||||
steam_id = user_data["steam_id"]
|
||||
steam_friend_code = str(int(steam_id) - STEAM_ID_OFFSET)
|
||||
|
||||
player_data = await get_user_data(steam_id, cache_path, config.proxy)
|
||||
names = await get_game_cn_names(
|
||||
[g.get("appid") for g in player_data["game_data"] if g.get("appid")],
|
||||
db_path,
|
||||
# 资料页抓取与近两周列表 API 并行;后者是可选增强,无配额时自动跳过
|
||||
player_data, recent_games = await asyncio.gather(
|
||||
get_user_data(steam_id, cache_path, config.proxy),
|
||||
get_recently_played(steam_id, config.proxy),
|
||||
)
|
||||
for g in player_data["game_data"]:
|
||||
g["game_name"] = localize_game_name(g["game_name"], g.get("appid"), names)
|
||||
draw_data = [
|
||||
recent_games = recent_games or []
|
||||
|
||||
# 游戏动态以最近两周列表为准(资料页只有最近几款),资料页解析的
|
||||
# 成就/最后运行日期按 appid 合并进来;列表没有的游戏用资料页顺序补齐
|
||||
page_games = {
|
||||
g["appid"]: g for g in player_data["game_data"] if g.get("appid")
|
||||
}
|
||||
rows = list(recent_games)
|
||||
known_appids = {r["appid"] for r in rows}
|
||||
rows += [
|
||||
{
|
||||
"game_header": game["game_image"],
|
||||
"game_name": game["game_name"],
|
||||
"game_time": f"{game['play_time']} 小时",
|
||||
"last_play_time": game["last_played"],
|
||||
"achievements": game["achievements"],
|
||||
"completed_achievement_number": game.get("completed_achievement_number"),
|
||||
"total_achievement_number": game.get("total_achievement_number"),
|
||||
"appid": appid,
|
||||
"name": game["game_name"],
|
||||
"two_week_hours": None,
|
||||
"total_hours": None,
|
||||
}
|
||||
for game in player_data["game_data"]
|
||||
for appid, game in page_games.items()
|
||||
if appid not in known_appids
|
||||
]
|
||||
rows = rows[:MAX_ACTIVITY_GAMES]
|
||||
|
||||
# 缺封面的行(不在资料页里)从公开 CDN 并发补,带缓存
|
||||
missing_appids = [r["appid"] for r in rows if r["appid"] not in page_games]
|
||||
cdn_headers = {}
|
||||
if missing_appids:
|
||||
fetched = await asyncio.gather(
|
||||
*(
|
||||
fetch_game_header_image(appid, cache_path, config.proxy)
|
||||
for appid in missing_appids
|
||||
)
|
||||
)
|
||||
cdn_headers = dict(zip(missing_appids, fetched))
|
||||
|
||||
names = await get_game_cn_names([r["appid"] for r in rows], db_path)
|
||||
draw_data = []
|
||||
for row in rows:
|
||||
page = page_games.get(row["appid"], {})
|
||||
header = page.get("game_image") or cdn_headers.get(row["appid"])
|
||||
if header is None:
|
||||
logger.warning(f"游戏 {row['appid']} 封面获取失败,本条跳过")
|
||||
continue
|
||||
if row["total_hours"] is not None:
|
||||
game_time = f"{row['total_hours']:.1f}"
|
||||
else:
|
||||
game_time = str(page.get("play_time") or "")
|
||||
draw_data.append(
|
||||
{
|
||||
"game_header": header,
|
||||
"game_name": localize_game_name(
|
||||
page.get("game_name") or row["name"], row["appid"], names
|
||||
),
|
||||
"game_time": f"{game_time} 小时",
|
||||
"last_play_time": page.get("last_played") or "",
|
||||
"achievements": page.get("achievements") or [],
|
||||
"completed_achievement_number": page.get(
|
||||
"completed_achievement_number"
|
||||
),
|
||||
"total_achievement_number": page.get("total_achievement_number"),
|
||||
"two_week_time": (
|
||||
f"近两周 {row['two_week_hours']:.1f} 小时"
|
||||
if row["two_week_hours"]
|
||||
else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# 近两周合计:API 列表求和最准;没有列表(资料不公开/无配额)时退回页面文案
|
||||
if recent_games:
|
||||
total_hours = sum(r["two_week_hours"] for r in recent_games)
|
||||
two_week_total = (
|
||||
f"{total_hours:.1f} 小时(过去 2 周)" if total_hours > 0 else "近两周未游玩"
|
||||
)
|
||||
else:
|
||||
two_week_total = player_data["recent_2_week_play_time"]
|
||||
|
||||
# 状态行/风控标记:资料页「当前正在游戏」优先,其次资料 XML;
|
||||
# XML 兜底路径的英文游戏名用映射表反查中文
|
||||
status = build_profile_status(
|
||||
player_data["online_state"],
|
||||
player_data["state_message"],
|
||||
player_data["privacy_state"],
|
||||
player_data["current_game"],
|
||||
)
|
||||
if status.get("game_name_en"):
|
||||
cn_name = _query_game_name_ch_by_en(db_path, status["game_name_en"])
|
||||
if cn_name:
|
||||
status["text"] = cn_name
|
||||
ban = describe_ban(
|
||||
player_data["vac_banned"],
|
||||
player_data["trade_ban_state"],
|
||||
player_data["limited_account"],
|
||||
)
|
||||
extra = {
|
||||
"level": player_data["level"],
|
||||
"counts": player_data["counts"],
|
||||
"member_since": player_data["member_since"],
|
||||
"status_text": status["text"],
|
||||
"status_kind": status["kind"],
|
||||
"ban_text": ban["text"] if ban else None,
|
||||
"ban_kind": ban["kind"] if ban else None,
|
||||
}
|
||||
image = draw_player_status(
|
||||
player_data["background"], player_data["avatar"], player_data["player_name"],
|
||||
str(steam_friend_code), player_data["description"],
|
||||
player_data["recent_2_week_play_time"], draw_data,
|
||||
two_week_total, draw_data,
|
||||
extra=extra,
|
||||
avatar_frame=player_data.get("avatar_frame"),
|
||||
)
|
||||
await info.finish(await UniMessage(Image(raw=image_to_bytes(image))).export(bot))
|
||||
|
||||
|
||||
Reference in new issue
Block a user