mirror of
https://github.com/PaiGramTeam/MibooGram.git
synced 2024-11-25 09:37:38 +00:00
✨ 支持深境螺旋推荐配队
Co-authored-by: 洛水居室 <luoshuijs@outlook.com>
This commit is contained in:
parent
b1455be303
commit
1541c8fabc
68
modules/apihelper/abyss_team.py
Normal file
68
modules/apihelper/abyss_team.py
Normal file
@ -0,0 +1,68 @@
|
||||
import time
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, validator, parse_obj_as
|
||||
|
||||
|
||||
class Member(BaseModel):
|
||||
star: int
|
||||
attr: str
|
||||
name: str
|
||||
|
||||
|
||||
class TeamRate(BaseModel):
|
||||
rate: float
|
||||
formation: List[Member]
|
||||
ownerNum: Optional[int]
|
||||
|
||||
@validator('rate', pre=True)
|
||||
def str2float(cls, v): # pylint: disable=R0201
|
||||
return float(v.replace('%', '')) / 100.0 if isinstance(v, str) else v
|
||||
|
||||
|
||||
class TeamRateResult(BaseModel):
|
||||
rateListUp: List[TeamRate]
|
||||
rateListDown: List[TeamRate]
|
||||
userCount: int
|
||||
|
||||
def sort(self, characters: List[str]):
|
||||
for team in self.rateListUp:
|
||||
team.ownerNum = len(set(characters) & {member.name for member in team.formation})
|
||||
for team in self.rateListDown:
|
||||
team.ownerNum = len(set(characters) & {member.name for member in team.formation})
|
||||
self.rateListUp.sort(key=lambda x: (x.ownerNum / 4 * x.rate), reverse=True)
|
||||
self.rateListDown.sort(key=lambda x: (x.ownerNum / 4 * x.rate), reverse=True)
|
||||
|
||||
|
||||
class AbyssTeamData:
|
||||
TEAM_RATE_API = "https://www.youchuang.fun/gamerole/formationRate"
|
||||
HEADERS = {
|
||||
'Host': 'www.youchuang.fun',
|
||||
'Referer': 'https://servicewechat.com/wxce4dbe0cb0f764b3/91/page-frame.html',
|
||||
'User-Agent': 'Mozilla/5.0 (iPad; CPU OS 15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) '
|
||||
'Mobile/15E148 MicroMessenger/8.0.20(0x1800142f) NetType/WIFI Language/zh_CN',
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
VERSION = "3.1"
|
||||
|
||||
def __init__(self):
|
||||
self.client = httpx.AsyncClient(headers=self.HEADERS)
|
||||
self.time = 0
|
||||
self.data = None
|
||||
self.ttl = 10 * 60
|
||||
|
||||
async def get_data(self) -> TeamRateResult:
|
||||
if self.data is None or self.time + self.ttl < time.time():
|
||||
data_up = await self.client.post(self.TEAM_RATE_API, json={"version": self.VERSION, "layer": 1})
|
||||
data_up_json = data_up.json()["result"]
|
||||
data_down = await self.client.post(self.TEAM_RATE_API, json={"version": self.VERSION, "layer": 2})
|
||||
data_down_json = data_down.json()["result"]
|
||||
self.data = TeamRateResult(rateListUp=parse_obj_as(List[TeamRate], data_up_json["rateList"]),
|
||||
rateListDown=parse_obj_as(List[TeamRate], data_down_json["rateList"]),
|
||||
userCount=data_up_json["userCount"])
|
||||
self.time = time.time()
|
||||
return self.data.copy(deep=True)
|
||||
|
||||
async def close(self):
|
||||
await self.client.aclose()
|
88
plugins/genshin/abyss_team.py
Normal file
88
plugins/genshin/abyss_team.py
Normal file
@ -0,0 +1,88 @@
|
||||
from telegram import Update, User
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters
|
||||
|
||||
from core.assets import AssetsService
|
||||
from core.baseplugin import BasePlugin
|
||||
from core.cookies.error import CookiesNotFoundError
|
||||
from core.plugin import Plugin, handler
|
||||
from core.template import TemplateService
|
||||
from core.user import UserService
|
||||
from core.user.error import UserNotFoundError
|
||||
from metadata.shortname import roleToId
|
||||
from modules.apihelper.abyss_team import AbyssTeamData
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.decorators.restricts import restricts
|
||||
from utils.helpers import get_genshin_client
|
||||
from utils.log import logger
|
||||
|
||||
|
||||
class AbyssTeam(Plugin, BasePlugin):
|
||||
"""深境螺旋推荐配队查询"""
|
||||
|
||||
def __init__(self, user_service: UserService = None, template_service: TemplateService = None,
|
||||
assets: AssetsService = None):
|
||||
self.template_service = template_service
|
||||
self.user_service = user_service
|
||||
self.assets_service = assets
|
||||
self.team_data = AbyssTeamData()
|
||||
|
||||
@staticmethod
|
||||
def _get_role_star_bg(value: int):
|
||||
if value == 4:
|
||||
return "./../abyss/background/roleStarBg4.png"
|
||||
elif value == 5:
|
||||
return "./../abyss/background/roleStarBg5.png"
|
||||
else:
|
||||
raise ValueError("错误的数据")
|
||||
|
||||
@staticmethod
|
||||
async def _get_data_from_user(user: User):
|
||||
try:
|
||||
logger.debug("尝试获取已绑定的原神账号")
|
||||
client = await get_genshin_client(user.id)
|
||||
logger.debug(f"获取成功, UID: {client.uid}")
|
||||
characters = await client.get_genshin_characters(client.uid)
|
||||
return [character.name for character in characters]
|
||||
except (UserNotFoundError, CookiesNotFoundError):
|
||||
logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息")
|
||||
return []
|
||||
|
||||
@handler(CommandHandler, command="abyss_team", block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^深渊推荐配队(.*)"), block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def command_start(self, update: Update, _: CallbackContext) -> None:
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
logger.info(f"用户 {user.full_name}[{user.id}] 查深渊推荐配队命令请求")
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
team_data = await self.team_data.get_data()
|
||||
# 尝试获取用户已绑定的原神账号信息
|
||||
user_data = await self._get_data_from_user(user)
|
||||
team_data.sort(user_data)
|
||||
abyss_team_data = {
|
||||
"up": [],
|
||||
"down": []
|
||||
}
|
||||
for i in team_data.rateListUp[0].formation:
|
||||
temp = {
|
||||
"icon": (await self.assets_service.character(roleToId(i.name)).icon()).as_uri(),
|
||||
"name": i.name,
|
||||
"background": self._get_role_star_bg(i.star),
|
||||
"hava": i.name in user_data,
|
||||
}
|
||||
abyss_team_data["up"].append(temp)
|
||||
for i in team_data.rateListDown[0].formation:
|
||||
temp = {
|
||||
"icon": (await self.assets_service.character(roleToId(i.name)).icon()).as_uri(),
|
||||
"name": i.name,
|
||||
"background": self._get_role_star_bg(i.star),
|
||||
"hava": i.name in user_data,
|
||||
}
|
||||
abyss_team_data["down"].append(temp)
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
png_data = await self.template_service.render('genshin/abyss_team', "abyss_team.html", abyss_team_data,
|
||||
{"width": 865, "height": 504}, full_page=False)
|
||||
await message.reply_photo(png_data, filename=f"abyss_team_{user.id}.png",
|
||||
allow_sending_without_reply=True)
|
@ -90,6 +90,11 @@
|
||||
<div class="command-name">/material</div>
|
||||
<div class="command-description">角色培养素材查询</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/abyss_team</div>
|
||||
<div class="command-description">深渊推荐配队</div>
|
||||
</div>
|
||||
|
||||
<div class="command">
|
||||
<div class="command-name">/hilichurls</div>
|
||||
<div class="command-description">丘丘语字典</div>
|
||||
|
69
resources/genshin/abyss_team/abyss_team.html
Normal file
69
resources/genshin/abyss_team/abyss_team.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>abyss</title>
|
||||
<link type="text/css" href="../../styles/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet">
|
||||
<style>
|
||||
#container {
|
||||
width: 865px;
|
||||
height: 504px;
|
||||
background: url("./../abyss/background/lookback-bg.png");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.character-icon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.character-side-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.item-not-owned:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 128px;
|
||||
height: 152px;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
z-index: 2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mx-auto flex flex-col h-full bg-no-repeat bg-cover" id="container">
|
||||
<div class="title text-2xl my-4 text-yellow-500 mx-auto">深境螺旋 - 推荐配队</div>
|
||||
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">第 12 层上半</div>
|
||||
<div class="mx-auto flex my-2">
|
||||
{% for i in up %}
|
||||
<div class="rounded-lg mx-2 overflow-hidden {% if not i.hava %}item-not-owned{% endif %}" style="background-color: rgb(233, 229, 220)">
|
||||
<div class="character-icon rounded-br-3xl bg-cover overflow-hidden"
|
||||
style="background: url({{i.background}});background-size: cover;">
|
||||
<img src="{{i.icon}}" alt=""></div>
|
||||
<div class="text-center">{{i.name}}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">第 12 层下半</div>
|
||||
<div class="mx-auto flex my-2">
|
||||
{% for i in down %}
|
||||
<div class="rounded-lg mx-2 overflow-hidden {% if not i.hava %}item-not-owned{% endif %}" style="background-color: rgb(233, 229, 220)">
|
||||
<div class="character-icon rounded-br-3xl bg-cover overflow-hidden"
|
||||
style="background: url({{i.background}});background-size: cover;">
|
||||
<img src="{{i.icon}}" alt=""></div>
|
||||
<div class="text-center">{{i.name}}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-2"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
100
resources/genshin/abyss_team/example.html
Normal file
100
resources/genshin/abyss_team/example.html
Normal file
@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>abyss</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
#container {
|
||||
width: 865px;
|
||||
height: 504px;
|
||||
background: url("../abyss/background/lookback-bg.png");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.character-icon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.character-side-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.item-not-owned:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 128px;
|
||||
height: 152px;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
z-index: 2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mx-auto flex flex-col h-full bg-no-repeat bg-cover" id="container">
|
||||
<div class="title text-2xl my-4 text-yellow-500 mx-auto">深境螺旋 - 推荐配队</div>
|
||||
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">第 12 层上半</div>
|
||||
<div class="mx-auto flex my-2">
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden item-not-owned">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg4.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg4.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg5.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg5.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="base-info flex flex-col px-20 py-1 text-black my-1">
|
||||
<div class="text-center mr-auto text-yellow-500">第 12 层下半</div>
|
||||
<div class="mx-auto flex my-2">
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg4.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg4.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg5.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg mx-2 overflow-hidden">
|
||||
<div class="character-icon rounded-br-3xl bg-cover"
|
||||
style="background: url('../abyss/background/roleStarBg5.png');background-size: cover;">
|
||||
<img src="./../../img/example/256x256.png" alt=""></div>
|
||||
<div class="text-center">xx</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-2"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
33
tests/test_abyss_team_data.py
Normal file
33
tests/test_abyss_team_data.py
Normal file
@ -0,0 +1,33 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from flaky import flaky
|
||||
|
||||
from modules.apihelper.abyss_team import AbyssTeamData, TeamRateResult, TeamRate
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def abyss_team_data():
|
||||
_abyss_team_data = AbyssTeamData()
|
||||
yield _abyss_team_data
|
||||
await _abyss_team_data.close()
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_abyss_team_data(abyss_team_data: AbyssTeamData):
|
||||
team_data = await abyss_team_data.get_data()
|
||||
assert isinstance(team_data, TeamRateResult)
|
||||
assert isinstance(team_data.rateListUp[0], TeamRate)
|
||||
assert isinstance(team_data.rateListUp[-1], TeamRate)
|
||||
assert isinstance(team_data.rateListDown[0], TeamRate)
|
||||
assert isinstance(team_data.rateListDown[-1], TeamRate)
|
||||
assert team_data.userCount > 0
|
||||
for i in team_data.rateListUp[0].formation:
|
||||
LOGGER.info("rate down info:name %s star %s", i.name, i.star)
|
||||
for i in team_data.rateListDown[0].formation:
|
||||
LOGGER.info("rate up info:name %s star %s", i.name, i.star)
|
Loading…
Reference in New Issue
Block a user