Support custom device

This commit is contained in:
omg-xtao 2023-06-17 11:48:36 +08:00 committed by GitHub
parent d0a917918d
commit c8bc3219c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 372 additions and 26 deletions

View File

@ -0,0 +1,38 @@
"""devices
Revision ID: a1c10da5704b
Revises: ddcfba3c7d5c
Create Date: 2023-06-13 19:34:47.189846
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "a1c10da5704b"
down_revision = "ddcfba3c7d5c"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"devices",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("account_id", sa.BigInteger(), nullable=True),
sa.Column("device_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("device_fp", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("device_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.PrimaryKeyConstraint("id"),
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("devices")
# ### end Alembic commands ###

View File

@ -0,0 +1,5 @@
"""DeviceService"""
from core.services.devices.services import DevicesService
__all__ = "DevicesService"

View File

@ -0,0 +1,23 @@
from typing import Optional
from sqlmodel import SQLModel, Field, Column, Integer, BigInteger
__all__ = ("Devices", "DevicesDataBase")
class Devices(SQLModel):
__table_args__ = dict(mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci")
id: Optional[int] = Field(default=None, sa_column=Column(Integer, primary_key=True, autoincrement=True))
account_id: int = Field(
default=None,
sa_column=Column(
BigInteger(),
),
)
device_id: str = Field()
device_fp: str = Field()
device_name: Optional[str] = Field(default=None)
class DevicesDataBase(Devices, table=True):
__tablename__ = "devices"

View File

@ -0,0 +1,41 @@
from typing import Optional
from sqlmodel import select
from core.base_service import BaseService
from core.dependence.database import Database
from core.services.devices.models import DevicesDataBase as Devices
from core.sqlmodel.session import AsyncSession
__all__ = ("DevicesRepository",)
class DevicesRepository(BaseService.Component):
def __init__(self, database: Database):
self.engine = database.engine
async def get(
self,
account_id: int,
) -> Optional[Devices]:
async with AsyncSession(self.engine) as session:
statement = select(Devices).where(Devices.account_id == account_id)
results = await session.exec(statement)
return results.first()
async def add(self, devices: Devices) -> None:
async with AsyncSession(self.engine) as session:
session.add(devices)
await session.commit()
async def update(self, devices: Devices) -> Devices:
async with AsyncSession(self.engine) as session:
session.add(devices)
await session.commit()
await session.refresh(devices)
return devices
async def delete(self, devices: Devices) -> None:
async with AsyncSession(self.engine) as session:
await session.delete(devices)
await session.commit()

View File

@ -0,0 +1,25 @@
from typing import Optional
from core.base_service import BaseService
from core.services.devices.repositories import DevicesRepository
from core.services.devices.models import DevicesDataBase as Devices
class DevicesService(BaseService):
def __init__(self, devices_repository: DevicesRepository) -> None:
self._repository: DevicesRepository = devices_repository
async def update(self, devices: Devices):
await self._repository.update(devices)
async def add(self, devices: Devices):
await self._repository.add(devices)
async def get(
self,
account_id: int,
) -> Optional[Devices]:
return await self._repository.get(account_id)
async def delete(self, devices: Devices) -> None:
return await self._repository.delete(devices)

View File

@ -11,7 +11,8 @@ from qrcode.image.pure import PyPNGImage
from ...logger import logger
from ...models.genshin.cookies import CookiesModel
from ...utility.helpers import get_device_id, get_ds, update_device_headers
from ...utility.devices import devices_methods
from ...utility.helpers import get_device_id, get_ds
__all__ = ("AuthClient",)
@ -92,7 +93,7 @@ class AuthClient:
"x-rpc-app_id": "bll8iq97cem8",
"User-Agent": "okhttp/4.8.0",
}
update_device_headers(self.user_id, headers)
await devices_methods.update_device_headers(self.user_id, headers)
app_version, client_type, ds_sign = get_ds(new_ds=True, data=data)
headers["x-rpc-app_version"] = app_version
headers["x-rpc-client_type"] = client_type

View File

@ -4,7 +4,8 @@ import time
from typing import Dict, Optional
from ..base.hyperionrequest import HyperionRequest
from ...utility.helpers import get_ua, get_ds, update_device_headers
from ...utility.devices import devices_methods
from ...utility.helpers import get_ua, get_ds
__all__ = ("Verify",)
@ -45,7 +46,7 @@ class Verify:
headers["Referer"] = referer
return headers
def get_headers(self, data: dict = None, params: dict = None):
async def get_headers(self, data: dict = None, params: dict = None):
headers = self.BBS_HEADERS.copy()
app_version, client_type, ds = get_ds(new_ds=True, data=data, params=params)
headers["x-rpc-app_version"] = app_version
@ -53,7 +54,7 @@ class Verify:
headers["DS"] = ds
headers["x-rpc-challenge_path"] = f"https://{self.HOST}{self.REFERER_URL}"
headers["x-rpc-challenge_game"] = self.GAME
update_device_headers(self.account_id, headers)
await devices_methods.update_device_headers(self.account_id, headers)
return headers
@staticmethod
@ -63,7 +64,7 @@ class Verify:
async def create(self, is_high: bool = False):
url = self.get_url(self.HOST, self.CREATE_VERIFICATION_URL)
params = {"is_high": "true" if is_high else "false"}
headers = self.get_headers(params=params)
headers = await self.get_headers(params=params)
response = await self.client.get(url, params=params, headers=headers)
return response
@ -71,7 +72,7 @@ class Verify:
url = self.get_url(self.HOST, self.VERIFY_VERIFICATION_URL)
data = {"geetest_challenge": challenge, "geetest_validate": validate, "geetest_seccode": f"{validate}|jordan"}
headers = self.get_headers(data=data)
headers = await self.get_headers(data=data)
response = await self.client.post(url, json=data, headers=headers)
return response

View File

@ -0,0 +1,33 @@
from typing import Optional, Dict
from core.services.devices import DevicesService
from modules.apihelper.utility.helpers import get_device_id, hex_digest
class DevicesMethods:
def __init__(self):
self.service: Optional[DevicesService] = None
@staticmethod
def get_default_device_header(account_id: int, headers: Dict = None) -> Dict[str, str]:
headers = headers or {}
headers["x-rpc-device_id"] = get_device_id(str(account_id))
headers["x-rpc-device_fp"] = hex_digest(headers["x-rpc-device_id"])[:13]
headers["x-rpc-device_name"] = "Xiaomi"
return headers
async def update_device_headers(self, account_id: int, headers: Dict = None) -> Dict[str, str]:
account_id = account_id or 0
if not self.service:
return self.get_default_device_header(account_id, headers)
device = await self.service.get(account_id)
if not device:
return self.get_default_device_header(account_id, headers)
headers = headers or {}
headers["x-rpc-device_id"] = device.device_id
headers["x-rpc-device_fp"] = device.device_fp
headers["x-rpc-device_name"] = device.device_name or "Xiaomi"
return headers
devices_methods = DevicesMethods()

View File

@ -4,9 +4,9 @@ import random
import string
import time
import uuid
from typing import Any, Mapping, Optional, Dict
from typing import Any, Mapping, Optional
__all__ = ("get_device_id", "hex_digest", "get_ds", "get_recognize_server", "get_ua", "update_device_headers")
__all__ = ("get_device_id", "hex_digest", "get_ds", "get_recognize_server", "get_ua")
RECOGNIZE_SERVER = {
"1": "cn_gf01",
@ -23,15 +23,6 @@ def get_device_id(name: str = ""):
return str(uuid.uuid3(uuid.NAMESPACE_URL, name))
def update_device_headers(account_id: int, headers: Dict = None) -> Dict[str, str]:
account_id = account_id or 0
headers = headers or {}
headers["x-rpc-device_id"] = get_device_id(str(account_id))
headers["x-rpc-device_fp"] = hex_digest(headers["x-rpc-device_id"])[:13]
headers["x-rpc-device_name"] = "Xiaomi"
return headers
def hex_digest(text):
_md5 = hashlib.md5() # nosec B303
_md5.update(text.encode())

187
plugins/account/devices.py Normal file
View File

@ -0,0 +1,187 @@
from typing import Optional
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, TelegramObject, Update
from telegram.ext import CallbackContext, ConversationHandler, filters
from telegram.helpers import escape_markdown
from core.basemodel import RegionEnum
from core.plugin import Plugin, conversation, handler
from core.services.cookies.services import CookiesService
from core.services.devices import DevicesService
from core.services.devices.models import DevicesDataBase as Devices
from core.services.players.services import PlayersService
from modules.apihelper.utility.devices import devices_methods
from utils.log import logger
__all__ = ("AccountDevicesPlugin",)
class AccountDevicesPluginData(TelegramObject):
device_id: str = ""
device_fp: str = ""
device_name: Optional[str] = None
account_id: int = 0
def reset(self):
self.device_id = ""
self.device_fp = ""
self.device_name = None
self.account_id = 0
CHECK_SERVER, INPUT_DEVICES, COMMAND_RESULT = range(10100, 10103)
class AccountDevicesPlugin(Plugin.Conversation):
"""设备绑定"""
def __init__(
self,
players_service: PlayersService = None,
cookies_service: CookiesService = None,
devices_service: DevicesService = None,
):
self.cookies_service = cookies_service
self.players_service = players_service
self.devices_service = devices_service
devices_methods.service = devices_service
@staticmethod
def parse_headers(data: AccountDevicesPluginData, headers_text: str) -> None:
headers = {}
for line in headers_text.splitlines():
if not line:
continue
try:
k, v = line.split(":", 1)
headers[k.strip()] = v.strip()
except ValueError:
continue
must_keys = {"x-rpc-device_id": 36, "x-rpc-device_fp": 13}
optional_keys = ["x-rpc-device_name"]
for k, v in must_keys.items():
if (k not in headers) or (not headers.get(k)):
raise ValueError
if len(headers.get(k)) != v:
raise ValueError
for k in optional_keys:
if k not in headers:
continue
elif headers.get(k) and len(headers.get(k)) > 64:
raise ValueError
data.device_id = headers.get("x-rpc-device_id")
data.device_fp = headers.get("x-rpc-device_fp")
data.device_name = headers.get("x-rpc-device_name")
@conversation.entry_point
@handler.command(command="setdevice", filters=filters.ChatType.PRIVATE, block=False)
@handler.command(command="setdevices", filters=filters.ChatType.PRIVATE, block=False)
async def command_start(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user
message = update.effective_message
logger.info("用户 %s[%s] 绑定设备命令请求", user.full_name, user.id)
account_devices_plugin_data: AccountDevicesPluginData = context.chat_data.get("account_devices_plugin_data")
if account_devices_plugin_data is None:
account_devices_plugin_data = AccountDevicesPluginData()
context.chat_data["account_devices_plugin_data"] = account_devices_plugin_data
else:
account_devices_plugin_data.reset()
text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择要绑定的服务器!或回复退出取消操作")}'
reply_keyboard = [["米游社"], ["退出"]]
await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True))
return CHECK_SERVER
@conversation.state(state=CHECK_SERVER)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False)
async def check_server(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user
message = update.effective_message
account_devices_plugin_data: AccountDevicesPluginData = context.chat_data.get("account_devices_plugin_data")
if message.text == "退出":
await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
if message.text == "米游社":
region = RegionEnum.HYPERION
bbs_name = "米游社"
else:
await message.reply_text("选择错误,请重新选择")
return CHECK_SERVER
player_info = await self.players_service.get(user.id, region=region)
if player_info:
cookies_database = await self.cookies_service.get(user.id, player_info.account_id, region)
if not cookies_database:
await message.reply_text(f"你还没有绑定 {bbs_name} 的Cookies请先绑定Cookies")
return ConversationHandler.END
account_devices_plugin_data.account_id = player_info.account_id
else:
await message.reply_text(f"你还没有绑定 {bbs_name} 的Cookies请先绑定Cookies")
return ConversationHandler.END
help_message = (
"<b>关于如何获取Device</b>\n"
"<b>此操作只能在PC上进行。</b>\n\n"
"PC\n"
"1、打开<a href='https://user.mihoyo.com/'>通行证</a>并登录\n"
"2、进入通行证按F12打开开发者工具\n"
"3、将开发者工具切换至网络(Network)并点击过滤栏中的 Fetch/XHR 并刷新页面\n"
"4、在请求列表中找到 login_by_cookie\n"
"5、右键并复制请求标头(Request Headers)\n"
"<u>如发现没有此请求大概因为缓存的存在需要你点击禁用缓存(Disable Cache)再次刷新页面</u>"
)
await message.reply_html(help_message, disable_web_page_preview=True)
await message.reply_text(f"请输入{bbs_name}的请求标头!或回复退出取消操作", reply_markup=ReplyKeyboardRemove())
return INPUT_DEVICES
@conversation.state(state=INPUT_DEVICES)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False)
async def input_cookies(self, update: Update, context: CallbackContext) -> int:
message = update.effective_message
user = update.effective_user
account_devices_plugin_data: AccountDevicesPluginData = context.chat_data.get("account_devices_plugin_data")
if message.text == "退出":
await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
try:
self.parse_headers(account_devices_plugin_data, message.text)
except ValueError as exc:
logger.info("用户 %s[%s] Devices解析出现错误\ntext:%s", user.full_name, user.id, message.text)
logger.debug("解析Devices出现错误", exc_info=exc)
await message.reply_text("解析Devices出现错误请检查是否正确", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
reply_keyboard = [["确认", "退出"]]
await message.reply_markdown_v2(
"请确认修改!", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
)
return COMMAND_RESULT
@conversation.state(state=COMMAND_RESULT)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False)
async def command_result(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user
message = update.effective_message
account_devices_plugin_data: AccountDevicesPluginData = context.chat_data.get("account_devices_plugin_data")
if message.text == "退出":
await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
if message.text == "确认":
device = await self.devices_service.get(account_devices_plugin_data.account_id)
if device:
device.device_id = account_devices_plugin_data.device_id
device.device_fp = account_devices_plugin_data.device_fp
device.device_name = account_devices_plugin_data.device_name
await self.devices_service.update(device)
logger.success("用户 %s[%s] 更新Devices", user.full_name, user.id)
else:
device = Devices(
account_id=account_devices_plugin_data.account_id,
device_id=account_devices_plugin_data.device_id,
device_fp=account_devices_plugin_data.device_fp,
device_name=account_devices_plugin_data.device_name,
)
await self.devices_service.add(device)
logger.info("用户 %s[%s] 绑定Devices成功", user.full_name, user.id)
await message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
await message.reply_text("回复错误,请重新输入")
return COMMAND_RESULT

View File

@ -4,7 +4,8 @@ from genshin import Client
from genshin.client.routes import InternationalRoute # noqa F401
from genshin.utility import recognize_starrail_server
from modules.apihelper.utility.helpers import hex_digest, get_ds, update_device_headers
from modules.apihelper.utility.devices import devices_methods
from modules.apihelper.utility.helpers import hex_digest, get_ds
AUTHKEY_API = "https://api-takumi.mihoyo.com/binding/api/genAuthKey"
HK4E_LOGIN_URL = InternationalRoute(
@ -37,7 +38,7 @@ async def get_authkey_by_stoken(client: Client) -> Optional[str]:
}
device_id = hex_digest(str(client.uid))
device = f"Paimon Build {device_id[:5]}"
update_device_headers(client.hoyolab_id, headers)
await devices_methods.update_device_headers(client.hoyolab_id, headers)
headers["x-rpc-device_name"] = device
headers["x-rpc-device_model"] = device
app_version, client_type, ds_sign = get_ds()

View File

@ -9,8 +9,8 @@ from genshin import constants, types, utility
from genshin.client import routes
from genshin.utility import generate_dynamic_secret, ds
from modules.apihelper.utility.helpers import get_ds, get_ua, get_device_id, hex_digest, update_device_headers
from utils.log import logger
from modules.apihelper.utility.devices import devices_methods
from modules.apihelper.utility.helpers import get_ds, get_ua, get_device_id, hex_digest
from utils.patch.methods import patch, patchable
DEVICE_ID = get_device_id()
@ -156,7 +156,7 @@ class BaseClient:
"x-rpc-client_type": client_type,
"ds": ds_sign,
}
update_device_headers(self.hoyolab_id, headers)
await devices_methods.update_device_headers(self.hoyolab_id, headers)
else:
raise TypeError(f"{region!r} is not a valid region.")
@ -190,7 +190,7 @@ class BaseClient:
headers = dict(headers or {})
headers.setdefault("User-Agent", self.USER_AGENT)
update_device_headers(self.hoyolab_id, headers)
await devices_methods.update_device_headers(self.hoyolab_id, headers)
if method is None:
method = "POST" if data else "GET"
@ -263,7 +263,7 @@ class BaseClient:
"ds": ds_sign,
}
headers.update(add_headers)
update_device_headers(self.hoyolab_id, headers)
await devices_methods.update_device_headers(self.hoyolab_id, headers)
elif self.region == types.Region.OVERSEAS:
headers.update(ds.get_ds_headers(data=data, params=params, region=region, lang=lang or self.lang))
headers["Referer"] = str(routes.BBS_REFERER_URL.get_url(self.region))
@ -348,7 +348,7 @@ class DailyRewardClient:
headers["x-rpc-challenge"] = challenge
headers["x-rpc-validate"] = validate
headers["x-rpc-seccode"] = f"{validate}|jordan"
update_device_headers(self.hoyolab_id, headers)
await devices_methods.update_device_headers(self.hoyolab_id, headers)
else:
raise TypeError(f"{self.region!r} is not a valid region.")