Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge branch dev into master #221

Merged
merged 48 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
710bf7f
Implement detailed Genshin character endpoint (#219)
FuriaPaladins Aug 21, 2024
01c5a8b
Fix error on get_genshin_user
seriaati Aug 21, 2024
8627aad
Add detailed character test
seriaati Aug 21, 2024
94226b0
Add ZZZPropertyType.ANOMALY_MASTERY
seriaati Aug 30, 2024
41002f9
Remove the usage of galias
seriaati Aug 31, 2024
b40f0a1
Remove custom validation dunder methods
seriaati Aug 31, 2024
d676bda
Remove the use of lang in APIModel
seriaati Aug 31, 2024
c296291
Fix validation error on FullGenshinUserStats
seriaati Aug 31, 2024
fa47d03
Fix validation error on CalculatorConsumable
seriaati Aug 31, 2024
3cc00b7
Fix TypeError on generic models
seriaati Aug 31, 2024
49d606a
Fix KeyError on GenshinDetailCharacters
seriaati Aug 31, 2024
e336225
Fix ValidationError on FullHonkaiUserStats
seriaati Aug 31, 2024
a25c76b
Fix ValidationError on LineupPreview
seriaati Aug 31, 2024
54070b5
Fix ValidationError on PartialLineupCharacter
seriaati Aug 31, 2024
a639c71
Revert PartialLineupCharacter changes
seriaati Aug 31, 2024
ddcb928
Fix ValidationError on PartialLineupCharacter
seriaati Aug 31, 2024
b594a40
Make stored_attendance_refresh_countdown optional
seriaati Aug 31, 2024
b03d8e6
Allow returning raw data
seriaati Aug 31, 2024
04c3340
Fix returning wrong icons
seriaati Aug 31, 2024
d515856
Fix _create_icon method
seriaati Aug 31, 2024
2630cf5
Fix problem with aliased fields in root validators
seriaati Aug 31, 2024
8d6abd7
Im dumb
seriaati Aug 31, 2024
dc844c3
Fix validation error on DailyReward
seriaati Aug 31, 2024
88fd504
Fix error in getting db char
seriaati Aug 31, 2024
00e3b1f
Add TheaterDifficulty.IDK
seriaati Sep 1, 2024
f45699e
Fix character icons with img theater
seriaati Sep 1, 2024
5287ec0
Remove partial/unknown character test
seriaati Sep 1, 2024
4071a1d
Add pyroculi
seriaati Sep 1, 2024
6b41fae
Remove messed up character test
seriaati Sep 1, 2024
70824fa
Add missing events in HSR announcements
seriaati Sep 1, 2024
1ed0efe
Add img field to Announcement
seriaati Sep 1, 2024
af17e53
Rename TheaterDifficulty.IDK to TheaterDifficulty.VISIONARY
seriaati Sep 1, 2024
e4ca614
Fix qrcode login
seriaati Sep 4, 2024
1fbc731
Change otp code mmt model to v4
seriaati Sep 4, 2024
dd355e4
Fix using tuple class to type hint
seriaati Sep 4, 2024
adfee94
Add support for SQLiteCache
seriaati Sep 5, 2024
bfaee2b
Add clear_cache method to SQLiteCache
seriaati Sep 5, 2024
ece46a2
Allow not passing in connection for SQLiteCache
seriaati Sep 5, 2024
b510928
Fix HSR gacha log route
seriaati Sep 5, 2024
5af9afd
Make wish record time timezone aware
seriaati Sep 5, 2024
5edba73
Export StarRailBannerType
seriaati Sep 5, 2024
fd20177
Fix tz offset causing KeyError in Genshin
seriaati Sep 5, 2024
5d53641
Add Natlan tribe reputations
seriaati Sep 10, 2024
aa8b729
Fix field not being Aliased
seriaati Sep 10, 2024
5e66268
Add ImgTheater battle stats
seriaati Sep 10, 2024
2add667
Fix HSR code redeem not working
seriaati Sep 10, 2024
a7c6080
Fix ValidationError in TheaterBattleStats
seriaati Sep 10, 2024
05b4429
Disable coverage workflows temporarily
seriaati Sep 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 38 additions & 38 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,44 +49,44 @@ jobs:
python -m nox -s test --verbose -- --cov-append
mv .coverage .coverage.${{ matrix.python-version }}

- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: .coverage.${{ matrix.python-version }}
retention-days: 1
if-no-files-found: error

upload-coverage:
needs: [test]
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Download coverage
uses: actions/download-artifact@v3
with:
name: coverage

- name: Combine coverage
run: |
pip install -r ./genshin-dev/pytest-requirements.txt
coverage combine
coverage xml -i

- name: Upload coverage to codeclimate
uses: paambaati/[email protected]
env:
CC_TEST_REPORTER_ID: cd8c7d84ae5f98d86882d666dce0946fe5aae1e63f442995bd9c6e17869e6513
with:
coverageLocations: .coverage.xml:coverage.py
# - name: Upload coverage
# uses: actions/upload-artifact@v3
# with:
# name: coverage
# path: .coverage.${{ matrix.python-version }}
# retention-days: 1
# if-no-files-found: error

# upload-coverage:
# needs: [test]
# runs-on: ubuntu-latest

# steps:
# - name: Checkout repo
# uses: actions/checkout@v4

# - name: Setup python 3.10
# uses: actions/setup-python@v5
# with:
# python-version: "3.10"

# - name: Download coverage
# uses: actions/download-artifact@v3
# with:
# name: coverage

# - name: Combine coverage
# run: |
# pip install -r ./genshin-dev/pytest-requirements.txt
# coverage combine
# coverage xml -i

# - name: Upload coverage to codeclimate
# uses: paambaati/[email protected]
# env:
# CC_TEST_REPORTER_ID: cd8c7d84ae5f98d86882d666dce0946fe5aae1e63f442995bd9c6e17869e6513
# with:
# coverageLocations: .coverage.xml:coverage.py

type-check:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ docs/pdoc
.mypy_cache/
.dmypy.json
dmypy.json

# databases
*.sqlite3
*.db
123 changes: 122 additions & 1 deletion genshin/client/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

if typing.TYPE_CHECKING:
import aioredis
import aiosqlite

__all__ = ["BaseCache", "Cache", "RedisCache", "StaticCache"]

__all__ = ["BaseCache", "Cache", "RedisCache", "StaticCache", "SQLiteCache"]

MINUTE = 60
HOUR = MINUTE * 60
Expand Down Expand Up @@ -200,3 +202,122 @@ async def set_static(self, key: typing.Any, value: typing.Any) -> None:
self.serialize_value(value),
ex=self.static_ttl,
)


class SQLiteCache(BaseCache):
"""SQLite implementation of the cache."""

conn: aiosqlite.Connection | None
ttl: int
static_ttl: int

def __init__(
self,
conn: aiosqlite.Connection | None = None,
*,
ttl: int = HOUR,
static_ttl: int = DAY,
db_name: str = "genshin_py.db",
) -> None:
self.conn = conn
self.ttl = ttl
self.static_ttl = static_ttl
self.db_name = db_name

async def _clear_cache(self, conn: aiosqlite.Connection) -> None:
"""Clear timed-out items."""
now = time.time()

await conn.execute("DELETE FROM cache WHERE expiration < ?", (now,))
await conn.commit()

async def initialize(self) -> None:
"""Initialize the cache."""
import aiosqlite

if self.conn is None:
conn = await aiosqlite.connect(self.db_name)
else:
conn = self.conn

await conn.execute("CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, expiration INTEGER)")
await conn.commit()

if self.conn is None:
await conn.close()

def serialize_key(self, key: typing.Any) -> str:
"""Serialize a key by turning it into a string."""
return str(key)

def serialize_value(self, value: typing.Any) -> str:
"""Serialize a value by turning it into a string."""
return json.dumps(value)

def deserialize_value(self, value: str) -> typing.Any:
"""Deserialize a value back into data."""
return json.loads(value)

async def get(self, key: typing.Any) -> typing.Optional[typing.Any]:
"""Get an object with a key."""
import aiosqlite

if self.conn is None:
conn = await aiosqlite.connect(self.db_name)
else:
conn = self.conn

async with conn.execute(
"SELECT value FROM cache WHERE key = ? AND expiration > ?", (self.serialize_key(key), int(time.time()))
) as cursor:
value = await cursor.fetchone()

if self.conn is None:
await conn.close()

if value is None:
return None

return self.deserialize_value(value[0])

async def set(self, key: typing.Any, value: typing.Any) -> None:
"""Save an object with a key."""
import aiosqlite

if self.conn is None:
conn = await aiosqlite.connect(self.db_name)
else:
conn = self.conn

await conn.execute(
"INSERT OR REPLACE INTO cache (key, value, expiration) VALUES (?, ?, ?)",
(self.serialize_key(key), self.serialize_value(value), int(time.time() + self.ttl)),
)
await conn.commit()
await self._clear_cache(conn)

if self.conn is None:
await conn.close()

async def get_static(self, key: typing.Any) -> typing.Optional[typing.Any]:
"""Get a static object with a key."""
return await self.get(key)

async def set_static(self, key: typing.Any, value: typing.Any) -> None:
"""Save a static object with a key."""
import aiosqlite

if self.conn is None:
conn = await aiosqlite.connect(self.db_name)
else:
conn = self.conn

await conn.execute(
"INSERT OR REPLACE INTO cache (key, value, expiration) VALUES (?, ?, ?)",
(self.serialize_key(key), self.serialize_value(value), int(time.time() + self.static_ttl)),
)
await conn.commit()
await self._clear_cache(conn)

if self.conn is None:
await conn.close()
29 changes: 6 additions & 23 deletions genshin/client/components/auth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from genshin.client import routes
from genshin.client.components import base
from genshin.client.manager import managers
from genshin.client.manager.cookie import fetch_cookie_token_with_game_token, fetch_stoken_with_game_token
from genshin.models.auth.cookie import (
AppLoginResult,
CNWebLoginResult,
Expand Down Expand Up @@ -247,35 +246,19 @@ async def login_with_qrcode(self) -> QRLoginResult:

scanned = False
while True:
check_result = await self._check_qrcode(
creation_result.app_id, creation_result.device_id, creation_result.ticket
)
if check_result.status == QRCodeStatus.SCANNED and not scanned:
status, cookies = await self._check_qrcode(creation_result.ticket)
if status is QRCodeStatus.SCANNED and not scanned:
LOGGER_.info("QR code scanned")
scanned = True
elif check_result.status == QRCodeStatus.CONFIRMED:
elif status is QRCodeStatus.CONFIRMED:
LOGGER_.info("QR code login confirmed")
break

await asyncio.sleep(2)

raw_data = check_result.payload.raw
assert raw_data is not None
await asyncio.sleep(1)

cookie_token = await fetch_cookie_token_with_game_token(
game_token=raw_data.game_token, account_id=raw_data.account_id
)
stoken = await fetch_stoken_with_game_token(game_token=raw_data.game_token, account_id=int(raw_data.account_id))

cookies = {
"stoken_v2": stoken.token,
"ltuid": stoken.aid,
"account_id": stoken.aid,
"ltmid": stoken.mid,
"cookie_token": cookie_token,
}
self.set_cookies(cookies)
return QRLoginResult(**cookies)
dict_cookies = {key: morsel.value for key, morsel in cookies.items()}
return QRLoginResult(**dict_cookies)

@managers.no_multi
async def create_mmt(self) -> MMT:
Expand Down
35 changes: 11 additions & 24 deletions genshin/client/components/auth/subclients/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
"""

import json
import random
import typing
from string import ascii_letters, digits
from http.cookies import SimpleCookie

import aiohttp

Expand All @@ -15,7 +14,7 @@
from genshin.client.components import base
from genshin.models.auth.cookie import AppLoginResult
from genshin.models.auth.geetest import SessionMMT, SessionMMTResult
from genshin.models.auth.qrcode import QRCodeCheckResult, QRCodeCreationResult
from genshin.models.auth.qrcode import QRCodeCreationResult, QRCodeStatus
from genshin.models.auth.verification import ActionTicket
from genshin.utility import auth as auth_utility
from genshin.utility import ds as ds_utility
Expand Down Expand Up @@ -180,46 +179,34 @@ async def _verify_email(self, code: str, ticket: ActionTicket) -> None:

async def _create_qrcode(self) -> QRCodeCreationResult:
"""Create a QR code for login."""
if self.default_game is None:
raise RuntimeError("No default game set.")

app_id = constants.APP_IDS[self.default_game][self.region]
device_id = "".join(random.choices(ascii_letters + digits, k=64))

async with aiohttp.ClientSession() as session:
async with session.post(
routes.CREATE_QRCODE_URL.get_url(),
json={"app_id": app_id, "device": device_id},
headers=auth_utility.QRCODE_HEADERS,
) as r:
data = await r.json()

if not data["data"]:
errors.raise_for_retcode(data)

url: str = data["data"]["url"]
return QRCodeCreationResult(
app_id=app_id,
ticket=url.split("ticket=")[1],
device_id=device_id,
url=url,
ticket=data["data"]["ticket"],
url=data["data"]["url"],
)

async def _check_qrcode(self, app_id: str, device_id: str, ticket: str) -> QRCodeCheckResult:
async def _check_qrcode(self, ticket: str) -> typing.Tuple[QRCodeStatus, SimpleCookie]:
"""Check the status of a QR code login."""
payload = {
"app_id": app_id,
"device": device_id,
"ticket": ticket,
}
payload = {"ticket": ticket}

async with aiohttp.ClientSession() as session:
async with session.post(
routes.CHECK_QRCODE_URL.get_url(),
json=payload,
headers=auth_utility.QRCODE_HEADERS,
) as r:
data = await r.json()

if not data["data"]:
errors.raise_for_retcode(data)
if not data["data"]:
errors.raise_for_retcode(data)

return QRCodeCheckResult(**data["data"])
return QRCodeStatus(data["data"]["status"]), r.cookies
6 changes: 3 additions & 3 deletions genshin/client/components/auth/subclients/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from genshin.client import routes
from genshin.client.components import base
from genshin.models.auth.cookie import CNWebLoginResult, MobileLoginResult, WebLoginResult
from genshin.models.auth.geetest import SessionMMT, SessionMMTResult
from genshin.models.auth.geetest import SessionMMT, SessionMMTResult, SessionMMTv4
from genshin.utility import auth as auth_utility
from genshin.utility import ds as ds_utility

Expand Down Expand Up @@ -164,7 +164,7 @@ async def _send_mobile_otp(
*,
encrypted: bool = False,
mmt_result: typing.Optional[SessionMMTResult] = None,
) -> typing.Union[None, SessionMMT]:
) -> typing.Union[None, SessionMMTv4]:
"""Attempt to send OTP to the provided mobile number.

May return aigis headers if captcha is triggered, None otherwise.
Expand Down Expand Up @@ -192,7 +192,7 @@ async def _send_mobile_otp(
if data["retcode"] == -3101:
# Captcha triggered
aigis = json.loads(r.headers["x-rpc-aigis"])
return SessionMMT(**aigis)
return SessionMMTv4(**aigis)

if not data["data"]:
errors.raise_for_retcode(data)
Expand Down
2 changes: 1 addition & 1 deletion genshin/client/components/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__(
device_id: typing.Optional[str] = None,
device_fp: typing.Optional[str] = None,
headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None,
cache: typing.Optional[client_cache.Cache] = None,
cache: typing.Optional[client_cache.BaseCache] = None,
debug: bool = False,
) -> None:
self.cookie_manager = managers.BaseCookieManager.from_cookies(cookies)
Expand Down
Loading
Loading