This commit is contained in:
0815Cracky
2024-02-27 11:46:37 +01:00
parent 6053e255ad
commit ff22f47b90
60 changed files with 7183 additions and 0 deletions

View File

@ -0,0 +1,315 @@
import copy
from enum import Enum, auto
from random import uniform
from millify import millify
#from TwitchChannelPointsMiner.utils import char_decision_as_index, float_round
from TwitchChannelPointsMiner.utils import float_round
class Strategy(Enum):
MOST_VOTED = auto()
HIGH_ODDS = auto()
PERCENTAGE = auto()
SMART_MONEY = auto()
SMART = auto()
def __str__(self):
return self.name
class Condition(Enum):
GT = auto()
LT = auto()
GTE = auto()
LTE = auto()
def __str__(self):
return self.name
class OutcomeKeys(object):
# Real key on Bet dict ['']
PERCENTAGE_USERS = "percentage_users"
ODDS_PERCENTAGE = "odds_percentage"
ODDS = "odds"
TOP_POINTS = "top_points"
# Real key on Bet dict [''] - Sum()
TOTAL_USERS = "total_users"
TOTAL_POINTS = "total_points"
# This key does not exist
DECISION_USERS = "decision_users"
DECISION_POINTS = "decision_points"
class DelayMode(Enum):
FROM_START = auto()
FROM_END = auto()
PERCENTAGE = auto()
def __str__(self):
return self.name
class FilterCondition(object):
__slots__ = [
"by",
"where",
"value",
]
def __init__(self, by=None, where=None, value=None, decision=None):
self.by = by
self.where = where
self.value = value
def __repr__(self):
return f"FilterCondition(by={self.by.upper()}, where={self.where}, value={self.value})"
class BetSettings(object):
__slots__ = [
"strategy",
"percentage",
"percentage_gap",
"max_points",
"minimum_points",
"stealth_mode",
"filter_condition",
"delay",
"delay_mode",
]
def __init__(
self,
strategy: Strategy = None,
percentage: int = None,
percentage_gap: int = None,
max_points: int = None,
minimum_points: int = None,
stealth_mode: bool = None,
filter_condition: FilterCondition = None,
delay: float = None,
delay_mode: DelayMode = None,
):
self.strategy = strategy
self.percentage = percentage
self.percentage_gap = percentage_gap
self.max_points = max_points
self.minimum_points = minimum_points
self.stealth_mode = stealth_mode
self.filter_condition = filter_condition
self.delay = delay
self.delay_mode = delay_mode
def default(self):
self.strategy = self.strategy if self.strategy is not None else Strategy.SMART
self.percentage = self.percentage if self.percentage is not None else 5
self.percentage_gap = (
self.percentage_gap if self.percentage_gap is not None else 20
)
self.max_points = self.max_points if self.max_points is not None else 50000
self.minimum_points = (
self.minimum_points if self.minimum_points is not None else 0
)
self.stealth_mode = (
self.stealth_mode if self.stealth_mode is not None else False
)
self.delay = self.delay if self.delay is not None else 6
self.delay_mode = (
self.delay_mode if self.delay_mode is not None else DelayMode.FROM_END
)
def __repr__(self):
return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, minimum_points={self.minimum_points}, stealth_mode={self.stealth_mode})"
class Bet(object):
__slots__ = ["outcomes", "decision", "total_users", "total_points", "settings"]
def __init__(self, outcomes: list, settings: BetSettings):
self.outcomes = outcomes
self.__clear_outcomes()
self.decision: dict = {}
self.total_users = 0
self.total_points = 0
self.settings = settings
def update_outcomes(self, outcomes):
for index in range(0, len(self.outcomes)):
self.outcomes[index][OutcomeKeys.TOTAL_USERS] = int(
outcomes[index][OutcomeKeys.TOTAL_USERS]
)
self.outcomes[index][OutcomeKeys.TOTAL_POINTS] = int(
outcomes[index][OutcomeKeys.TOTAL_POINTS]
)
if outcomes[index]["top_predictors"] != []:
# Sort by points placed by other users
outcomes[index]["top_predictors"] = sorted(
outcomes[index]["top_predictors"],
key=lambda x: x["points"],
reverse=True,
)
# Get the first elements (most placed)
top_points = outcomes[index]["top_predictors"][0]["points"]
self.outcomes[index][OutcomeKeys.TOP_POINTS] = top_points
# Inefficient, but otherwise outcomekeys are represented wrong
self.total_points = 0
self.total_users = 0
for index in range(0, len(self.outcomes)):
self.total_users += self.outcomes[index][OutcomeKeys.TOTAL_USERS]
self.total_points += self.outcomes[index][OutcomeKeys.TOTAL_POINTS]
if (
self.total_users > 0
and self.total_points > 0
):
for index in range(0, len(self.outcomes)):
self.outcomes[index][OutcomeKeys.PERCENTAGE_USERS] = float_round(
(100 * self.outcomes[index][OutcomeKeys.TOTAL_USERS]) / self.total_users
)
self.outcomes[index][OutcomeKeys.ODDS] = float_round(
#self.total_points / max(self.outcomes[index][OutcomeKeys.TOTAL_POINTS], 1)
0
if self.outcomes[index][OutcomeKeys.TOTAL_POINTS] == 0
else self.total_points / self.outcomes[index][OutcomeKeys.TOTAL_POINTS]
)
self.outcomes[index][OutcomeKeys.ODDS_PERCENTAGE] = float_round(
#100 / max(self.outcomes[index][OutcomeKeys.ODDS], 1)
0
if self.outcomes[index][OutcomeKeys.ODDS] == 0
else 100 / self.outcomes[index][OutcomeKeys.ODDS]
)
self.__clear_outcomes()
def __repr__(self):
return f"Bet(total_users={millify(self.total_users)}, total_points={millify(self.total_points)}), decision={self.decision})\n\t\tOutcome A({self.get_outcome(0)})\n\t\tOutcome B({self.get_outcome(1)})"
def get_decision(self, parsed=False):
#decision = self.outcomes[0 if self.decision["choice"] == "A" else 1]
decision = self.outcomes[self.decision["choice"]]
return decision if parsed is False else Bet.__parse_outcome(decision)
@staticmethod
def __parse_outcome(outcome):
return f"{outcome['title']} ({outcome['color']}), Points: {millify(outcome[OutcomeKeys.TOTAL_POINTS])}, Users: {millify(outcome[OutcomeKeys.TOTAL_USERS])} ({outcome[OutcomeKeys.PERCENTAGE_USERS]}%), Odds: {outcome[OutcomeKeys.ODDS]} ({outcome[OutcomeKeys.ODDS_PERCENTAGE]}%)"
def get_outcome(self, index):
return Bet.__parse_outcome(self.outcomes[index])
def __clear_outcomes(self):
for index in range(0, len(self.outcomes)):
keys = copy.deepcopy(list(self.outcomes[index].keys()))
for key in keys:
if key not in [
OutcomeKeys.TOTAL_USERS,
OutcomeKeys.TOTAL_POINTS,
OutcomeKeys.TOP_POINTS,
OutcomeKeys.PERCENTAGE_USERS,
OutcomeKeys.ODDS,
OutcomeKeys.ODDS_PERCENTAGE,
"title",
"color",
"id",
]:
del self.outcomes[index][key]
for key in [
OutcomeKeys.PERCENTAGE_USERS,
OutcomeKeys.ODDS,
OutcomeKeys.ODDS_PERCENTAGE,
OutcomeKeys.TOP_POINTS,
]:
if key not in self.outcomes[index]:
self.outcomes[index][key] = 0
'''def __return_choice(self, key) -> str:
return "A" if self.outcomes[0][key] > self.outcomes[1][key] else "B"'''
def __return_choice(self, key) -> int:
largest=0
for index in range(0, len(self.outcomes)):
if self.outcomes[index][key] > self.outcomes[largest][key]:
largest = index
return largest
def skip(self) -> bool:
if self.settings.filter_condition is not None:
# key == by , condition == where
key = self.settings.filter_condition.by
condition = self.settings.filter_condition.where
value = self.settings.filter_condition.value
fixed_key = (
key
if key not in [OutcomeKeys.DECISION_USERS, OutcomeKeys.DECISION_POINTS]
else key.replace("decision", "total")
)
if key in [OutcomeKeys.TOTAL_USERS, OutcomeKeys.TOTAL_POINTS]:
compared_value = (
self.outcomes[0][fixed_key] + self.outcomes[1][fixed_key]
)
else:
#outcome_index = char_decision_as_index(self.decision["choice"])
outcome_index = self.decision["choice"]
compared_value = self.outcomes[outcome_index][fixed_key]
# Check if condition is satisfied
if condition == Condition.GT:
if compared_value > value:
return False, compared_value
elif condition == Condition.LT:
if compared_value < value:
return False, compared_value
elif condition == Condition.GTE:
if compared_value >= value:
return False, compared_value
elif condition == Condition.LTE:
if compared_value <= value:
return False, compared_value
return True, compared_value # Else skip the bet
else:
return False, 0 # Default don't skip the bet
def calculate(self, balance: int) -> dict:
self.decision = {"choice": None, "amount": 0, "id": None}
if self.settings.strategy == Strategy.MOST_VOTED:
self.decision["choice"] = self.__return_choice(OutcomeKeys.TOTAL_USERS)
elif self.settings.strategy == Strategy.HIGH_ODDS:
self.decision["choice"] = self.__return_choice(OutcomeKeys.ODDS)
elif self.settings.strategy == Strategy.PERCENTAGE:
self.decision["choice"] = self.__return_choice(OutcomeKeys.ODDS_PERCENTAGE)
elif self.settings.strategy == Strategy.SMART_MONEY:
self.decision["choice"] = self.__return_choice(OutcomeKeys.TOP_POINTS)
elif self.settings.strategy == Strategy.SMART:
difference = abs(
self.outcomes[0][OutcomeKeys.PERCENTAGE_USERS]
- self.outcomes[1][OutcomeKeys.PERCENTAGE_USERS]
)
self.decision["choice"] = (
self.__return_choice(OutcomeKeys.ODDS)
if difference < self.settings.percentage_gap
else self.__return_choice(OutcomeKeys.TOTAL_USERS)
)
if self.decision["choice"] is not None:
#index = char_decision_as_index(self.decision["choice"])
index = self.decision["choice"]
self.decision["id"] = self.outcomes[index]["id"]
self.decision["amount"] = min(
int(balance * (self.settings.percentage / 100)),
self.settings.max_points,
)
if (
self.settings.stealth_mode is True
and self.decision["amount"]
>= self.outcomes[index][OutcomeKeys.TOP_POINTS]
):
reduce_amount = uniform(1, 5)
self.decision["amount"] = (
self.outcomes[index][OutcomeKeys.TOP_POINTS] - reduce_amount
)
self.decision["amount"] = int(self.decision["amount"])
return self.decision

View File

@ -0,0 +1,74 @@
from datetime import datetime
from TwitchChannelPointsMiner.classes.entities.Drop import Drop
from TwitchChannelPointsMiner.classes.Settings import Settings
class Campaign(object):
__slots__ = [
"id",
"game",
"name",
"status",
"in_inventory",
"end_at",
"start_at",
"dt_match",
"drops",
"channels",
]
def __init__(self, dict):
self.id = dict["id"]
self.game = dict["game"]
self.name = dict["name"]
self.status = dict["status"]
self.channels = (
[]
if dict["allow"]["channels"] is None
else list(map(lambda x: x["id"], dict["allow"]["channels"]))
)
self.in_inventory = False
self.end_at = datetime.strptime(dict["endAt"], "%Y-%m-%dT%H:%M:%SZ")
self.start_at = datetime.strptime(dict["startAt"], "%Y-%m-%dT%H:%M:%SZ")
self.dt_match = self.start_at < datetime.now() < self.end_at
self.drops = list(map(lambda x: Drop(x), dict["timeBasedDrops"]))
def __repr__(self):
return f"Campaign(id={self.id}, name={self.name}, game={self.game}, in_inventory={self.in_inventory})"
def __str__(self):
return (
f"{self.name}, Game: {self.game['displayName']} - Drops: {len(self.drops)} pcs. - In inventory: {self.in_inventory}"
if Settings.logger.less
else self.__repr__()
)
def clear_drops(self):
self.drops = list(
filter(lambda x: x.dt_match is True and x.is_claimed is False, self.drops)
)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.id == other.id
else:
return False
def sync_drops(self, drops, callback):
# Iterate all the drops from inventory
for drop in drops:
# Iterate all the drops from out campaigns array
# After id match update with:
# [currentMinutesWatched, hasPreconditionsMet, dropInstanceID, isClaimed]
for i in range(len(self.drops)):
current_id = self.drops[i].id
if drop["id"] == current_id:
self.drops[i].update(drop["self"])
# If after update we all conditions are meet we can claim the drop
if self.drops[i].is_claimable is True:
claimed = callback(self.drops[i])
self.drops[i].is_claimed = claimed
break

View File

@ -0,0 +1,103 @@
from datetime import datetime
from TwitchChannelPointsMiner.classes.Settings import Settings
from TwitchChannelPointsMiner.utils import percentage
class Drop(object):
__slots__ = [
"id",
"name",
"benefit",
"minutes_required",
"has_preconditions_met",
"current_minutes_watched",
"drop_instance_id",
"is_claimed",
"is_claimable",
"percentage_progress",
"end_at",
"start_at",
"dt_match",
"is_printable",
]
def __init__(self, dict):
self.id = dict["id"]
self.name = dict["name"]
self.benefit = ", ".join(
list(set([bf["benefit"]["name"] for bf in dict["benefitEdges"]]))
)
self.minutes_required = dict["requiredMinutesWatched"]
self.has_preconditions_met = None # [True, False], None we don't know
self.current_minutes_watched = 0
self.drop_instance_id = None
self.is_claimed = False
self.is_claimable = False
self.is_printable = False
self.percentage_progress = 0
self.end_at = datetime.strptime(dict["endAt"], "%Y-%m-%dT%H:%M:%SZ")
self.start_at = datetime.strptime(dict["startAt"], "%Y-%m-%dT%H:%M:%SZ")
self.dt_match = self.start_at < datetime.now() < self.end_at
def update(
self,
progress,
):
self.has_preconditions_met = progress["hasPreconditionsMet"]
updated_percentage = percentage(
progress["currentMinutesWatched"], self.minutes_required
)
quarter = round((updated_percentage / 25), 4).is_integer()
self.is_printable = (
# The new currentMinutesWatched are GT than previous
progress["currentMinutesWatched"] > self.current_minutes_watched
and (
# The drop is printable when we have a new updated values and:
# - also the percentage It's different and quarter is True (self.current_minutes_watched != 0 for skip boostrap phase)
# - or we have watched 1 and the previous value is 0 - We are collecting a new drop :)
(
updated_percentage > self.percentage_progress
and quarter is True
and self.current_minutes_watched != 0
)
or (
progress["currentMinutesWatched"] == 1
and self.current_minutes_watched == 0
)
)
)
self.current_minutes_watched = progress["currentMinutesWatched"]
self.drop_instance_id = progress["dropInstanceID"]
self.is_claimed = progress["isClaimed"]
self.is_claimable = (
self.is_claimed is False and self.drop_instance_id is not None
)
self.percentage_progress = updated_percentage
def __repr__(self):
return f"Drop(id={self.id}, name={self.name}, benefit={self.benefit}, minutes_required={self.minutes_required}, has_preconditions_met={self.has_preconditions_met}, current_minutes_watched={self.current_minutes_watched}, percentage_progress={self.percentage_progress}%, drop_instance_id={self.drop_instance_id}, is_claimed={self.is_claimed})"
def __str__(self):
return (
f"{self.name} ({self.benefit}) {self.current_minutes_watched}/{self.minutes_required} ({self.percentage_progress}%)"
if Settings.logger.less
else self.__repr__()
)
def progress_bar(self):
progress = self.percentage_progress // 2
remaining = (100 - self.percentage_progress) // 2
if remaining + progress < 50:
remaining += 50 - (remaining + progress)
return f"|{('' * progress)}{(' ' * remaining)}|\t{self.percentage_progress}% [{self.current_minutes_watched}/{self.minutes_required}]"
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.id == other.id
else:
return False

View File

@ -0,0 +1,94 @@
from TwitchChannelPointsMiner.classes.entities.Bet import Bet
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer
from TwitchChannelPointsMiner.classes.Settings import Settings
from TwitchChannelPointsMiner.utils import _millify, float_round
class EventPrediction(object):
__slots__ = [
"streamer",
"event_id",
"title",
"created_at",
"prediction_window_seconds",
"status",
"result",
"box_fillable",
"bet_confirmed",
"bet_placed",
"bet",
]
def __init__(
self,
streamer: Streamer,
event_id,
title,
created_at,
prediction_window_seconds,
status,
outcomes,
):
self.streamer = streamer
self.event_id = event_id
self.title = title.strip()
self.created_at = created_at
self.prediction_window_seconds = prediction_window_seconds
self.status = status
self.result: dict = {"string": "", "type": None, "gained": 0}
self.box_fillable = False
self.bet_confirmed = False
self.bet_placed = False
self.bet = Bet(outcomes, streamer.settings.bet)
def __repr__(self):
return f"EventPrediction(event_id={self.event_id}, streamer={self.streamer}, title={self.title})"
def __str__(self):
return (
f"EventPrediction: {self.streamer} - {self.title}"
if Settings.logger.less
else self.__repr__()
)
def elapsed(self, timestamp):
return float_round((timestamp - self.created_at).total_seconds())
def closing_bet_after(self, timestamp):
return float_round(self.prediction_window_seconds - self.elapsed(timestamp))
def print_recap(self) -> str:
return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.result['string']}"
def parse_result(self, result) -> dict:
result_type = result["type"]
points = {}
points["placed"] = (
self.bet.decision["amount"] if result_type != "REFUND" else 0
)
points["won"] = (
result["points_won"]
if result["points_won"] or result_type == "REFUND"
else 0
)
points["gained"] = (
points["won"] - points["placed"] if result_type != "REFUND" else 0
)
points["prefix"] = "+" if points["gained"] >= 0 else ""
action = (
"Lost"
if result_type == "LOSE"
else ("Refunded" if result_type == "REFUND" else "Gained")
)
self.result = {
"string": f"{result_type}, {action}: {points['prefix']}{_millify(points['gained'])}",
"type": result_type,
"gained": points["gained"],
}
return points

View File

@ -0,0 +1,69 @@
import json
from TwitchChannelPointsMiner.utils import server_time
class Message(object):
__slots__ = [
"topic",
"topic_user",
"message",
"type",
"data",
"timestamp",
"channel_id",
"identifier",
]
def __init__(self, data):
self.topic, self.topic_user = data["topic"].split(".")
self.message = json.loads(data["message"])
self.type = self.message["type"]
self.data = self.message["data"] if "data" in self.message else None
self.timestamp = self.__get_timestamp()
self.channel_id = self.__get_channel_id()
self.identifier = f"{self.type}.{self.topic}.{self.channel_id}"
def __repr__(self):
return f"{self.message}"
def __str__(self):
return f"{self.message}"
def __get_timestamp(self):
return (
server_time(self.message)
if self.data is None
else (
self.data["timestamp"]
if "timestamp" in self.data
else server_time(self.data)
)
)
def __get_channel_id(self):
return (
self.topic_user
if self.data is None
else (
self.data["prediction"]["channel_id"]
if "prediction" in self.data
else (
self.data["claim"]["channel_id"]
if "claim" in self.data
else (
self.data["channel_id"]
if "channel_id" in self.data
else (
self.data["balance"]["channel_id"]
if "balance" in self.data
else self.topic_user
)
)
)
)
)

View File

@ -0,0 +1,16 @@
class PubsubTopic(object):
__slots__ = ["topic", "user_id", "streamer"]
def __init__(self, topic, user_id=None, streamer=None):
self.topic = topic
self.user_id = user_id
self.streamer = streamer
def is_user_topic(self):
return self.streamer is None
def __str__(self):
if self.is_user_topic():
return f"{self.topic}.{self.user_id}"
else:
return f"{self.topic}.{self.streamer.channel_id}"

View File

@ -0,0 +1,12 @@
class Raid(object):
__slots__ = ["raid_id", "target_login"]
def __init__(self, raid_id, target_login):
self.raid_id = raid_id
self.target_login = target_login
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.raid_id == other.raid_id
else:
return False

View File

@ -0,0 +1,107 @@
import json
import logging
import time
from base64 import b64encode
from TwitchChannelPointsMiner.classes.Settings import Settings
from TwitchChannelPointsMiner.constants import DROP_ID
logger = logging.getLogger(__name__)
class Stream(object):
__slots__ = [
"broadcast_id",
"title",
"game",
"tags",
"drops_tags",
"campaigns",
"campaigns_ids",
"viewers_count",
"spade_url",
"payload",
"watch_streak_missing",
"minute_watched",
"__last_update",
"__minute_watched_timestamp",
]
def __init__(self):
self.broadcast_id = None
self.title = None
self.game = {}
self.tags = []
self.drops_tags = False
self.campaigns = []
self.campaigns_ids = []
self.viewers_count = 0
self.__last_update = 0
self.spade_url = None
self.payload = None
self.init_watch_streak()
def encode_payload(self) -> dict:
json_event = json.dumps(self.payload, separators=(",", ":"))
return {"data": (b64encode(json_event.encode("utf-8"))).decode("utf-8")}
def update(self, broadcast_id, title, game, tags, viewers_count):
self.broadcast_id = broadcast_id
self.title = title.strip()
self.game = game
# #343 temporary workaround
self.tags = tags or []
# ------------------------
self.viewers_count = viewers_count
self.drops_tags = (
DROP_ID in [tag["id"] for tag in self.tags] and self.game != {}
)
self.__last_update = time.time()
logger.debug(f"Update: {self}")
def __repr__(self):
return f"Stream(title={self.title}, game={self.__str_game()}, tags={self.__str_tags()})"
def __str__(self):
return f"{self.title}" if Settings.logger.less else self.__repr__()
def __str_tags(self):
return (
None
if self.tags == []
else ", ".join([tag["localizedName"] for tag in self.tags])
)
def __str_game(self):
return None if self.game in [{}, None] else self.game["displayName"]
def game_name(self):
return None if self.game in [{}, None] else self.game["name"]
def game_id(self):
return None if self.game in [{}, None] else self.game["id"]
def update_required(self):
return self.__last_update == 0 or self.update_elapsed() >= 120
def update_elapsed(self):
return 0 if self.__last_update == 0 else (time.time() - self.__last_update)
def init_watch_streak(self):
self.watch_streak_missing = True
self.minute_watched = 0
self.__minute_watched_timestamp = 0
def update_minute_watched(self):
if self.__minute_watched_timestamp != 0:
self.minute_watched += round(
(time.time() - self.__minute_watched_timestamp) / 60, 5
)
self.__minute_watched_timestamp = time.time()

View File

@ -0,0 +1,284 @@
import json
import logging
import os
import time
from datetime import datetime
from threading import Lock
from TwitchChannelPointsMiner.classes.Chat import ChatPresence, ThreadChat
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings, DelayMode
from TwitchChannelPointsMiner.classes.entities.Stream import Stream
from TwitchChannelPointsMiner.classes.Settings import Events, Settings
from TwitchChannelPointsMiner.constants import URL
from TwitchChannelPointsMiner.utils import _millify
logger = logging.getLogger(__name__)
class StreamerSettings(object):
__slots__ = [
"make_predictions",
"follow_raid",
"claim_drops",
"claim_moments",
"watch_streak",
"bet",
"chat",
]
def __init__(
self,
make_predictions: bool = None,
follow_raid: bool = None,
claim_drops: bool = None,
claim_moments: bool = None,
watch_streak: bool = None,
bet: BetSettings = None,
chat: ChatPresence = None,
):
self.make_predictions = make_predictions
self.follow_raid = follow_raid
self.claim_drops = claim_drops
self.claim_moments = claim_moments
self.watch_streak = watch_streak
self.bet = bet
self.chat = chat
def default(self):
for name in [
"make_predictions",
"follow_raid",
"claim_drops",
"claim_moments",
"watch_streak",
]:
if getattr(self, name) is None:
setattr(self, name, True)
if self.bet is None:
self.bet = BetSettings()
if self.chat is None:
self.chat = ChatPresence.ONLINE
def __repr__(self):
return f"BetSettings(make_predictions={self.make_predictions}, follow_raid={self.follow_raid}, claim_drops={self.claim_drops}, claim_moments={self.claim_moments}, watch_streak={self.watch_streak}, bet={self.bet}, chat={self.chat})"
class Streamer(object):
__slots__ = [
"username",
"channel_id",
"settings",
"is_online",
"stream_up",
"online_at",
"offline_at",
"channel_points",
"minute_watched_requests",
"viewer_is_mod",
"activeMultipliers",
"irc_chat",
"stream",
"raid",
"history",
"streamer_url",
"mutex",
]
def __init__(self, username, settings=None):
self.username: str = username.lower().strip()
self.channel_id: str = ""
self.settings = settings
self.is_online = False
self.stream_up = 0
self.online_at = 0
self.offline_at = 0
self.channel_points = 0
self.minute_watched_requests = None
self.viewer_is_mod = False
self.activeMultipliers = None
self.irc_chat = None
self.stream = Stream()
self.raid = None
self.history = {}
self.streamer_url = f"{URL}/{self.username}"
self.mutex = Lock()
def __repr__(self):
return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})"
def __str__(self):
return (
f"{self.username} ({_millify(self.channel_points)} points)"
if Settings.logger.less
else self.__repr__()
)
def set_offline(self):
if self.is_online is True:
self.offline_at = time.time()
self.is_online = False
self.toggle_chat()
logger.info(
f"{self} is Offline!",
extra={
"emoji": ":sleeping:",
"event": Events.STREAMER_OFFLINE,
},
)
def set_online(self):
if self.is_online is False:
self.online_at = time.time()
self.is_online = True
self.stream.init_watch_streak()
self.toggle_chat()
logger.info(
f"{self} is Online!",
extra={
"emoji": ":partying_face:",
"event": Events.STREAMER_ONLINE,
},
)
def print_history(self):
return ", ".join(
[
f"{key}({self.history[key]['counter']} times, {_millify(self.history[key]['amount'])} gained)"
for key in sorted(self.history)
if self.history[key]["counter"] != 0
]
)
def update_history(self, reason_code, earned, counter=1):
if reason_code not in self.history:
self.history[reason_code] = {"counter": 0, "amount": 0}
self.history[reason_code]["counter"] += counter
self.history[reason_code]["amount"] += earned
if reason_code == "WATCH_STREAK":
self.stream.watch_streak_missing = False
def stream_up_elapsed(self):
return self.stream_up == 0 or ((time.time() - self.stream_up) > 120)
def drops_condition(self):
return (
self.settings.claim_drops is True
and self.is_online is True
# and self.stream.drops_tags is True
and self.stream.campaigns_ids != []
)
def viewer_has_points_multiplier(self):
return self.activeMultipliers is not None and len(self.activeMultipliers) > 0
def total_points_multiplier(self):
return (
sum(
map(
lambda x: x["factor"],
self.activeMultipliers,
),
)
if self.activeMultipliers is not None
else 0
)
def get_prediction_window(self, prediction_window_seconds):
delay_mode = self.settings.bet.delay_mode
delay = self.settings.bet.delay
if delay_mode == DelayMode.FROM_START:
return min(delay, prediction_window_seconds)
elif delay_mode == DelayMode.FROM_END:
return max(prediction_window_seconds - delay, 0)
elif delay_mode == DelayMode.PERCENTAGE:
return prediction_window_seconds * delay
else:
return prediction_window_seconds
# === ANALYTICS === #
def persistent_annotations(self, event_type, event_text):
event_type = event_type.upper()
if event_type in ["WATCH_STREAK", "WIN", "PREDICTION_MADE", "LOSE"]:
primary_color = (
"#45c1ff" # blue #45c1ff yellow #ffe045 green #36b535 red #ff4545
if event_type == "WATCH_STREAK"
else ("#ffe045" if event_type == "PREDICTION_MADE" else ("#36b535" if event_type == "WIN" else "#ff4545"))
)
data = {
"borderColor": primary_color,
"label": {
"style": {"color": "#000", "background": primary_color},
"text": event_text,
},
}
self.__save_json("annotations", data)
def persistent_series(self, event_type="Watch"):
self.__save_json("series", event_type=event_type)
def __save_json(self, key, data={}, event_type="Watch"):
# https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript
now = datetime.now().replace(microsecond=0)
data.update({"x": round(datetime.timestamp(now) * 1000)})
if key == "series":
data.update({"y": self.channel_points})
if event_type is not None:
data.update({"z": event_type.replace("_", " ").title()})
fname = os.path.join(Settings.analytics_path, f"{self.username}.json")
temp_fname = fname + '.temp' # Temporary file name
with self.mutex:
# Create and write to the temporary file
with open(temp_fname, "w") as temp_file:
json_data = json.load(
open(fname, "r")) if os.path.isfile(fname) else {}
if key not in json_data:
json_data[key] = []
json_data[key].append(data)
json.dump(json_data, temp_file, indent=4)
# Replace the original file with the temporary file
os.replace(temp_fname, fname)
def leave_chat(self):
if self.irc_chat is not None:
self.irc_chat.stop()
# Recreate a new thread to start again
# raise RuntimeError("threads can only be started once")
self.irc_chat = ThreadChat(
self.irc_chat.username,
self.irc_chat.token,
self.username,
)
def __join_chat(self):
if self.irc_chat is not None:
if self.irc_chat.is_alive() is False:
self.irc_chat.start()
def toggle_chat(self):
if self.settings.chat == ChatPresence.ALWAYS:
self.__join_chat()
elif self.settings.chat != ChatPresence.NEVER:
if self.is_online is True:
if self.settings.chat == ChatPresence.ONLINE:
self.__join_chat()
elif self.settings.chat == ChatPresence.OFFLINE:
self.leave_chat()
else:
if self.settings.chat == ChatPresence.ONLINE:
self.leave_chat()
elif self.settings.chat == ChatPresence.OFFLINE:
self.__join_chat()