Files
TwitchDropsMiner/inventory.py

165 lines
5.4 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Optional, List, Dict, TYPE_CHECKING
from constants import JsonType, GQL_OPERATIONS
if TYPE_CHECKING:
from twitch import Twitch
class Game:
def __init__(self, data: JsonType):
self.id: int = int(data["id"])
self.name: str = data["name"]
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f"Game({self.id}, {self.name})"
def __eq__(self, other: object):
if isinstance(other, self.__class__):
return self.id == other.id
return NotImplemented
def __hash__(self) -> int:
return hash((self.__class__.__name__, self.id))
class BaseDrop:
def __init__(self, campaign: DropsCampaign, data: JsonType):
self._twitch: Twitch = campaign._twitch
self.id: str = data["id"]
self.name: str = data["name"]
self.campaign: DropsCampaign = campaign
self.rewards: List[str] = [b["benefit"]["name"] for b in data["benefitEdges"]]
self.starts_at: datetime = datetime.strptime(data["startAt"], "%Y-%m-%dT%H:%M:%SZ")
self.ends_at: datetime = datetime.strptime(data["endAt"], "%Y-%m-%dT%H:%M:%SZ")
# If claim_id is not None, we can use it to claim the drop
self.claim_id: Optional[str] = data["self"]["dropInstanceID"]
self.is_claimed: bool = data["self"]["isClaimed"]
self._preconditions: bool = data["self"]["hasPreconditionsMet"]
@property
def can_earn(self) -> bool:
return (
self._preconditions # preconditions are met
and self.campaign.active # campaign is active
and not self.is_claimed # drop isn't already claimed
and self.starts_at <= datetime.utcnow() < self.ends_at # it's within the timeframe
)
@property
def can_claim(self) -> bool:
return self.claim_id is not None
def rewards_text(self, delim: str = ", ") -> str:
return delim.join(self.rewards)
async def claim(self) -> bool:
"""
Returns True if the claim succeeded, False otherwise.
"""
if not self.can_claim:
return False
if self.is_claimed:
return True
op = GQL_OPERATIONS["ClaimDrop"].with_variables(
{"input": {"dropInstanceID": self.claim_id}}
)
response = await self._twitch.gql_request(op)
data = response["data"]
if "errors" in data and data["errors"]:
return False
elif "claimDropRewards" in data:
if not data["claimDropRewards"]:
return False
elif (
data["claimDropRewards"]["status"]
in ["ELIGIBLE_FOR_ALL", "DROP_INSTANCE_ALREADY_CLAIMED"]
):
self.is_claimed = True
return True
return False
class TimedDrop(BaseDrop):
def __init__(self, campaign: DropsCampaign, data: JsonType):
super().__init__(campaign, data)
self.current_minutes: int = data["self"]["currentMinutesWatched"]
self.required_minutes: int = data["requiredMinutesWatched"]
if self.is_claimed:
# claimed drops report 0 current minutes, so we need to make a correction
self.current_minutes = self.required_minutes
@property
def remaining_minutes(self) -> int:
return self.required_minutes - self.current_minutes
@property
def progress(self) -> float:
return self.current_minutes / self.required_minutes
async def claim(self) -> bool:
result = await super().claim()
if result:
self.current_minutes = self.required_minutes
return result
def update_claim(self, claim_id: str):
self.claim_id = claim_id
def update_minutes(self, minutes: int):
self.current_minutes = minutes
def display(self, *, countdown: bool = True):
self.campaign._twitch.gui.progress.display(self, countdown=countdown)
def bump_minutes(self):
if self.current_minutes < self.required_minutes:
self.current_minutes += 1
class DropsCampaign:
def __init__(self, twitch: Twitch, data: JsonType):
self._twitch: Twitch = twitch
self.id: str = data["id"]
self.name: str = data["name"]
self.game: Game = Game(data["game"])
self.starts_at: datetime = datetime.strptime(data["startAt"], "%Y-%m-%dT%H:%M:%SZ")
self.ends_at: datetime = datetime.strptime(data["endAt"], "%Y-%m-%dT%H:%M:%SZ")
self.status: str = data["status"]
self.timed_drops: Dict[str, TimedDrop] = {
d["id"]: TimedDrop(self, d) for d in data["timeBasedDrops"]
}
@property
def active(self):
return self.status == "ACTIVE"
@property
def total_drops(self) -> int:
return len(self.timed_drops)
@property
def claimed_drops(self) -> int:
return sum(d.is_claimed for d in self.timed_drops.values())
@property
def remaining_drops(self) -> int:
return sum(not d.is_claimed for d in self.timed_drops.values())
@property
def remaining_minutes(self) -> int:
return sum(d.remaining_minutes for d in self.timed_drops.values())
@property
def progress(self) -> float:
return sum(d.progress for d in self.timed_drops.values()) / self.total_drops
def get_drop(self, drop_id: str) -> Optional[TimedDrop]:
return self.timed_drops.get(drop_id)