Support Akasha System

This commit is contained in:
omg-xtao 2023-11-03 20:36:14 +08:00 committed by GitHub
parent 144bd22359
commit cf478b3b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 667 additions and 0 deletions

View File

@ -0,0 +1,138 @@
from types import TracebackType
from typing import Optional, Type, List
from urllib.parse import unquote
import httpx
from modules.apihelper.models.genshin.akasha import (
AkashaRank,
AkashaLeaderboardCategory,
AkashaLeaderboard,
AkashaSubStat,
AkashaArtifact,
)
BASE_URL = "https://akasha.cv/api"
MAIN_API = BASE_URL + "/filters/accounts/"
RANK_API = BASE_URL + "/getCalculationsForUser/"
DATA_API = BASE_URL + "/user/"
REFRESH_API = BASE_URL + "/user/refresh/"
LEADERBOARD_API = BASE_URL + "/leaderboards"
LEADERBOARD_CATEGORY_API = BASE_URL + "/v2/leaderboards/categories"
ARTIFACTS_API = BASE_URL + "/artifacts"
class Akasha:
SUB_STAT_MAP = {
AkashaSubStat.CRR: "critValue",
AkashaSubStat.ATK: "substats.ATK%",
AkashaSubStat.HP: "substats.HP%",
AkashaSubStat.DEF: "substats.DEF%",
AkashaSubStat.ATKF: "substats.Flat ATK",
AkashaSubStat.HPF: "substats.Flat HP",
AkashaSubStat.DEFF: "substats.Flat DEF",
AkashaSubStat.EM: "substats.Elemental Mastery",
AkashaSubStat.ER: "substats.Energy Recharge",
AkashaSubStat.CR: "substats.Crit RATE",
AkashaSubStat.CD: "substats.Crit DMG",
}
SUB_STAT_NAME_MAP = {
"Flat ATK": "攻击力",
"Flat HP": "血量",
"Flat DEF": "防御力",
"ATK%": "百分比攻击力",
"HP%": "百分比血量",
"DEF%": "百分比防御",
"Elemental Mastery": "元素精通",
"Energy Recharge": "元素充能效率",
"Crit RATE": "暴击率",
"Crit DMG": "暴击伤害",
"Cryo DMG Bonus": "冰元素伤害加成",
"Pyro DMG Bonus": "火元素伤害加成",
"Hydro DMG Bonus": "水元素伤害加成",
"Electro DMG Bonus": "雷元素伤害加成",
"Anemo DMG Bonus": "风元素伤害加成",
"Geo DMG Bonus": "岩元素伤害加成",
"Dendro DMG Bonus": "草元素伤害加成",
"Healing Bonus": "治疗加成",
"Physical Bonus": "物理伤害加成",
}
def __init__(self):
self.client = httpx.AsyncClient(timeout=60)
self.session_id = None
async def get_session_id(self) -> Optional[str]:
if self.session_id is None:
resp = await self.client.get(MAIN_API)
sid = resp.cookies.get("connect.sid", "")
sid = unquote(str(sid))
self.session_id = sid.split(".")[0].split(":")[-1]
return self.session_id
async def refresh_user_data(self, uid: int) -> None:
session_id = await self.get_session_id()
params = {"sessionID": session_id}
await self.client.get(DATA_API + str(uid), params=params)
await self.client.get(REFRESH_API + str(uid), params=params)
async def get_rank_data(self, uid: int) -> List[AkashaRank]:
await self.refresh_user_data(uid)
try:
resp = await self.client.get(RANK_API + str(uid))
data = resp.json()["data"]
except KeyError:
return []
return [AkashaRank(**i) for i in data]
async def get_leaderboard_categories(self, character_id: int) -> List[AkashaLeaderboardCategory]:
params = {"characterId": character_id}
try:
resp = await self.client.get(LEADERBOARD_CATEGORY_API, params=params)
data = resp.json()["data"]
except KeyError:
return []
return [AkashaLeaderboardCategory(**i) for i in data]
async def get_leaderboard(self, calculation_id: str, uid: int = None) -> List[AkashaLeaderboard]:
params = {
"sort": "calculation.result",
"p": "",
"calculationId": calculation_id,
"order": -1,
"size": 20,
"page": 1,
"filter": "",
"uids": "",
"fromId": "",
}
if uid:
params["uids"] = f"[uid]{uid}"
try:
resp = await self.client.get(LEADERBOARD_API, params=params)
data = resp.json()["data"]
except KeyError:
return []
return [AkashaLeaderboard(**i) for i in data]
async def get_artifacts_list(self, sort_by: AkashaSubStat = AkashaSubStat.CRR) -> List[AkashaArtifact]:
params = {
"sort": self.SUB_STAT_MAP[sort_by],
"p": "",
}
try:
resp = await self.client.get(ARTIFACTS_API, params=params)
data = resp.json()["data"]
except KeyError:
return []
return [AkashaArtifact(**i) for i in data]
async def __aenter__(self):
return self
async def __aexit__(
self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
):
if self.client.is_closed:
return
await self.client.aclose()

View File

@ -0,0 +1,212 @@
from datetime import datetime
from enum import Enum
from typing import Dict, List, Any, Optional
from pydantic import BaseModel, Field
class AkashaSubStat(str, Enum):
CRR = "双暴"
ATK = "百分比攻击力"
HP = "百分比生命值"
DEF = "百分比防御力"
ATKF = "固定攻击力"
HPF = "固定生命值"
DEFF = "固定防御力"
EM = "元素精通"
ER = "元素充能效率"
CR = "暴击率"
CD = "暴击伤害"
class AkashaRankCalFit(BaseModel):
calculationId: str
short: str
name: str
details: str
result: float
ranking: str
outOf: int
priority: int
type: str
class AkashaRankCal(BaseModel):
fit: AkashaRankCalFit
class AkashaRank(BaseModel):
_id: str
characterId: int
uid = int
constellation: int
icon: str
class AkashaLeaderboardCategoryWeapon(BaseModel):
name: str
icon: str
substat: str
type: str
rarity: str
refinement: int
calculationId: str
details: str
class AkashaLeaderboardCategory(BaseModel):
_id: str
name: str
addDate: datetime
c6: str
characterId: int
characterName: str
count: int
details: str
element: str
new: int
rarity: int
short: str
weapons: List[AkashaLeaderboardCategoryWeapon]
weaponsCount: int
characterIcon: str
index: int
class AkashaLeaderboardCalculation(BaseModel):
id: str
result: float
@property
def int(self) -> int:
return int(self.result)
class AkashaLeaderboardArtifactSet(BaseModel):
icon: str
count: int
class AkashaLeaderboardOwner(BaseModel):
nickname: str
adventureRank: float
profilePicture: Any
nameCard: str
patreon: Dict[str, Any]
region: str
class AkashaLeaderboardStatsValue(BaseModel):
value: float
@property
def int(self) -> int:
return int(self.value)
@property
def percent(self) -> str:
return f"{self.value * 100:.1f}"
@property
def web_value(self) -> str:
return f"{self.value:.2f}"
class AkashaLeaderboardStats(BaseModel):
maxHp: AkashaLeaderboardStatsValue
atk: AkashaLeaderboardStatsValue
def_: AkashaLeaderboardStatsValue = Field(..., alias="def")
elementalMastery: AkashaLeaderboardStatsValue
energyRecharge: AkashaLeaderboardStatsValue
healingBonus: AkashaLeaderboardStatsValue
critRate: AkashaLeaderboardStatsValue
critDamage: AkashaLeaderboardStatsValue
electroDamageBonus: Optional[AkashaLeaderboardStatsValue]
class AkashaLeaderboardWeaponInfo(BaseModel):
level: int
promoteLevel: int
refinementLevel: AkashaLeaderboardStatsValue
class AkashaLeaderboardWeapon(BaseModel):
weaponInfo: AkashaLeaderboardWeaponInfo
flat: Dict[str, Any]
name: str
icon: str
class AkashaLeaderboardCharacterMetadata(BaseModel):
element: str
class AkashaLeaderboard(BaseModel):
_id: str
calculation: AkashaLeaderboardCalculation
characterId: int
type: str
uid: str
artifactObjects: Dict[str, Any]
artifactSets: Dict[str, AkashaLeaderboardArtifactSet]
calculations: Dict[str, Any]
constellation: int
costumeId: str
critValue: float
md5: str
name: str
owner: AkashaLeaderboardOwner
propMap: Dict[str, Any]
proudSkillExtraLevelMap: Dict[str, Any]
stats: AkashaLeaderboardStats
talentsLevelMap: Dict[str, Any]
weapon: AkashaLeaderboardWeapon
icon: str
index: str
nameCardLink: str
profilePictureLink: str
characterMetadata: AkashaLeaderboardCharacterMetadata
class AkashaArtifactType(str, Enum):
BRACER = "EQUIP_BRACER"
"""生之花"""
NECKLACE = "EQUIP_NECKLACE"
"""死之羽"""
SHOES = "EQUIP_SHOES"
"""时之沙"""
RING = "EQUIP_RING"
"""空之杯"""
DRESS = "EQUIP_DRESS"
"""理之冠"""
@property
def real_name(self):
name_map = {
"EQUIP_BRACER": "生之花",
"EQUIP_NECKLACE": "死之羽",
"EQUIP_SHOES": "时之沙",
"EQUIP_RING": "空之杯",
"EQUIP_DRESS": "理之冠",
}
return name_map[self.value]
class AkashaArtifact(BaseModel):
_id: str
uid: int
critValue: float
equipType: AkashaArtifactType
icon: str
level: int
mainStatKey: str
mainStatValue: float
name: str
owner: AkashaLeaderboardOwner
setName: str
stars: int
substats: Dict[str, float]
substatsIdList: List[int]
index: int
nameCardLink: str
profilePictureLink: str

112
plugins/genshin/akasha.py Normal file
View File

@ -0,0 +1,112 @@
from typing import TYPE_CHECKING, Optional
from telegram.constants import ChatAction
from telegram.ext import filters
from core.dependence.assets import AssetsService
from core.plugin import Plugin, handler
from core.services.template.models import FileType
from core.services.template.services import TemplateService
from gram_core.services.players import PlayersService
from metadata.genshin import AVATAR_DATA
from metadata.shortname import roleToName, roleToId
from modules.apihelper.client.components.akasha import Akasha
from utils.log import logger
if TYPE_CHECKING:
from telegram import Update
from telegram.ext import ContextTypes
class AkashaPlugin(Plugin):
"""Akasha 数据排行"""
def __init__(
self,
assets_service: AssetsService = None,
template_service: TemplateService = None,
player_service: PlayersService = None,
) -> None:
self.assets_service = assets_service
self.template_service = template_service
self.player_service = player_service
async def get_user_uid(self, user_id: int) -> Optional[int]:
player = await self.player_service.get(user_id)
if player is None:
return None
return player.player_id
@staticmethod
async def get_leaderboard_data(character_id: int, uid: int = None):
akasha = Akasha()
categories = await akasha.get_leaderboard_categories(character_id)
if len(categories) == 0 or len(categories[0].weapons) == 0:
raise NotImplementedError
calculation_id = categories[0].weapons[0].calculationId
count = categories[0].count
data = await akasha.get_leaderboard(calculation_id)
if len(data) == 0:
raise NotImplementedError
user_data = []
if uid:
user_data = await akasha.get_leaderboard(calculation_id, uid)
if len(user_data) == 0:
data = [data]
else:
data = [user_data, data]
return data, count
async def get_avatar_board_render_data(self, character: str, uid: int):
character_id = roleToId(character)
name_card = (await self.assets_service.namecard(character_id).navbar()).as_uri()
avatar = (await self.assets_service.avatar(character_id).icon()).as_uri()
rarity = 5
try:
rarity = {k: v["rank"] for k, v in AVATAR_DATA.items()}[str(character_id)]
except KeyError:
logger.warning("未找到角色 %s 的星级", character_id)
akasha_data, count = await self.get_leaderboard_data(character_id, uid)
return {
"character": character, # 角色名
"avatar": avatar, # 角色头像
"namecard": name_card, # 角色名片
"rarity": rarity, # 角色稀有度
"count": count,
"all_data": akasha_data,
}
@handler.command("avatar_board", block=False)
@handler.message(filters.Regex(r"^角色排名(.*)$"), block=False)
async def avatar_board(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"):
user = update.effective_user
message = update.effective_message
args = self.get_args(context)
if len(args) == 0:
reply_message = await message.reply_text("请指定要查询的角色")
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
return
avatar_name = roleToName(args[0])
uid = await self.get_user_uid(user.id)
try:
render_data = await self.get_avatar_board_render_data(avatar_name, uid)
except NotImplementedError:
reply_message = await message.reply_text("暂不支持该角色")
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
return
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
image = await self.template_service.render(
"genshin/akasha/char_rank.jinja2",
render_data,
viewport={"width": 1040, "height": 500},
full_page=True,
query_selector=".container",
file_type=FileType.PHOTO,
ttl=24 * 60 * 60,
)
await image.reply_photo(message)

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Avatar List</title>
<link type="text/css" href="./style.css" rel="stylesheet"/>
<link type="text/css" href="../avatar_list/style.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
<style>
.avatar > div::after {
background-position: center center;
background-size: cover;
background-image: url("../../background/rarity/half/{{ rarity }}.png");
}
</style>
</head>
<body>
<div class="container">
<div class="head" style="background-image: url('{{ namecard }}')">
<div class="avatar">
<div><img src="{{ avatar }}" alt="avatar"></div>
</div>
<div class="player">
<div>
<div class="nickname">{{ character }}</div>
<div class="uid">角色排行 共 {{ count }} 条数据</div>
</div>
</div>
<div class="logo"></div>
</div>
{% for akasha_data in all_data %}
<div class="content">
<div class="row">
<div>#</div>
<div style="flex: 3">UID</div>
<div style="flex: 3">昵称</div>
<div style="flex: 3">暴击 : 暴伤</div>
<div style="flex: 2">生命</div>
<div style="flex: 2">攻击</div>
<div style="flex: 2">防御</div>
<div style="flex: 2">精通</div>
<div style="flex: 2">充能</div>
<div style="flex: 2">HYPER</div>
</div>
{% for data in akasha_data %}
<div
{% if loop.index is even %}
class="row second-row"
{% else %}
class="row"
{% endif %}
>
<div>{{ data.index }}</div>
<div style="flex: 3">{{ data.uid }}</div>
<div style="flex: 3" class="username">{{ data.owner.nickname }}</div>
<div style="flex: 3">{{ data.stats.critRate.percent }} : {{ data.stats.critDamage.percent }}</div>
<div style="flex: 2">{{ data.stats.maxHp.int }}</div>
<div style="flex: 2">{{ data.stats.atk.int }}</div>
<div style="flex: 2">{{ data.stats.def_.int }}</div>
<div style="flex: 2">{{ data.stats.elementalMastery.int }}</div>
<div style="flex: 2">{{ data.stats.energyRecharge.percent }}%</div>
<div style="flex: 2; background-color: rgb(229 171 229/70%)">{{ data.calculation.int }}</div>
</div>
{% endfor %}
</div>
{% if loop.index == 1 %}
<div style="height: 50px"></div>
{% endif %}
{% endfor %}
</div>
</body>
</html>

View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Avatar List</title>
<link type="text/css" href="./style.css" rel="stylesheet"/>
<link type="text/css" href="../avatar_list/style.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
<style>
.avatar > div::after {
background-position: center center;
background-size: cover;
background-image: url("../../background/rarity/half/5.png");
}
</style>
</head>
<body>
<div class="container">
<div class="head" style="background-image: url('../../assets/namecard/210081/navbar.png')">
<div class="avatar">
<div><img src="../../assets/avatar/10000002/icon.png" alt="avatar"></div>
</div>
<div class="player">
<div>
<div class="nickname">神里绫华</div>
<div class="uid">角色排行 共 199999 条数据</div>
</div>
</div>
<div class="logo"></div>
</div>
<div class="content">
<div class="row">
<div>#</div>
<div style="flex: 3">UID</div>
<div style="flex: 3">昵称</div>
<div style="flex: 3">暴击 : 暴伤</div>
<div style="flex: 2">生命值</div>
<div style="flex: 2">攻击力</div>
<div style="flex: 2">防御力</div>
<div style="flex: 2">精通</div>
<div style="flex: 2">充能</div>
<div style="flex: 2">HYPER</div>
</div>
<div class="row">
<div>1</div>
<div style="flex: 3">2222222</div>
<div style="flex: 3" class="username">
得得得得得得得得得
</div>
<div style="flex: 3">7.7 : 50.0</div>
<div style="flex: 2">20000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">1000</div>
<div style="flex: 2">300.0%</div>
<div style="flex: 2; background-color: rgb(229 171 229/70%)">451399</div>
</div>
</div>
<div style="height: 50px"></div>
<div class="content">
<div class="row">
<div>#</div>
<div style="flex: 3">UID</div>
<div style="flex: 3">昵称</div>
<div style="flex: 3">暴击 : 暴伤</div>
<div style="flex: 2">生命值</div>
<div style="flex: 2">攻击力</div>
<div style="flex: 2">防御力</div>
<div style="flex: 2">精通</div>
<div style="flex: 2">充能</div>
<div style="flex: 2">HYPER</div>
</div>
<div class="row">
<div>1</div>
<div style="flex: 3">2222222</div>
<div style="flex: 3" class="username">
得得得得得得得得得
</div>
<div style="flex: 3">7.7 : 50.0</div>
<div style="flex: 2">20000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">1000</div>
<div style="flex: 2">300.0%</div>
<div style="flex: 2; background-color: rgb(229 171 229/70%)">451399</div>
</div>
<div class="row second-row">
<div>2</div>
<div style="flex: 3">
<div>11111111</div>
</div>
<div style="flex: 3" class="username">
啊啊啊啊啊啊啊啊啊啊啊啊
</div>
<div style="flex: 3">7.7 : 50.0</div>
<div style="flex: 2">20000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">1000</div>
<div style="flex: 2">300.0%</div>
<div style="flex: 2">451398</div>
</div>
<div class="row">
<div>3</div>
<div style="flex: 3">
<div>3333333</div>
</div>
<div style="flex: 3" class="username">
姑姑姑姑姑姑过过过过
</div>
<div style="flex: 3">7.7 : 50.0</div>
<div style="flex: 2">20000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">2000</div>
<div style="flex: 2">1000</div>
<div style="flex: 2">300.0%</div>
<div style="flex: 2">451399</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,5 @@
.username {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}