Files
Twitch-Drops-Miner/TwitchChannelPointsMiner/classes/WebSocketsPool.py

180 lines
7.2 KiB
Python

import logging
import threading
import time
import json
from dateutil import parser
from TwitchChannelPointsMiner.classes.EventPrediction import EventPrediction
from TwitchChannelPointsMiner.classes.Raid import Raid
from TwitchChannelPointsMiner.classes.TwitchWebSocket import TwitchWebSocket
logger = logging.getLogger(__name__)
def get_streamer_index(streamers, channel_id):
return next(
i
for i, x in enumerate(streamers)
if x.channel_id == channel_id
)
# You can't listen for more than 50 topics at once
class WebSocketsPool:
def __init__(self, twitch, twitch_browser, streamers):
self.ws = None
self.twitch = twitch
self.twitch_browser = twitch_browser
self.streamers = streamers
def submit(self, topic):
if self.ws is None or len(self.ws.topics) >= 50:
self.create_new_websocket()
self.ws.topics.append(topic)
if not self.ws.is_opened:
self.ws.pending_topics.append(topic)
else:
self.ws.listen(topic, self.twitch.twitch_login.get_auth_token())
def create_new_websocket(self):
self.ws = TwitchWebSocket(
"wss://pubsub-edge.twitch.tv/v1",
on_message=WebSocketsPool.on_message,
on_open=WebSocketsPool.on_open,
on_close=WebSocketsPool.handle_websocket_reconnection,
)
self.ws.parent_pool = self
self.ws.is_closed = False
self.ws.is_opened = False
self.ws.topics = []
self.ws.pending_topics = []
self.ws.twitch = self.twitch
self.ws.twitch_browser = self.twitch_browser
self.ws.streamers = self.streamers
self.ws.events_predictions = {}
self.ws.last_message_time = 0
self.ws.last_message_type = None
threading.Thread(target=lambda: self.ws.run_forever()).start()
@staticmethod
def on_open(ws):
def run():
ws.is_opened = True
ws.ping()
for topic in ws.pending_topics:
ws.listen(topic, ws.twitch.twitch_login.get_auth_token())
while not ws.is_closed:
ws.ping()
time.sleep(30)
threading.Thread(target=run).start()
@staticmethod
def handle_websocket_reconnection(ws):
ws.is_closed = True
logger.info("Reconnecting to Twitch PubSub server in 30 seconds")
time.sleep(30)
self = ws.parent_pool
if self.ws == ws:
self.ws = None
for topic in ws.topics:
self.submit(topic)
@staticmethod
def on_message(ws, message):
logger.debug(f"Received: {message.strip()}")
response = json.loads(message)
if response["type"] == "MESSAGE":
data = response["data"]
topic, topic_user = data["topic"].split(".")
message = json.loads(data["message"])
message_type = message["type"]
message_data = None
if "data" in message:
message_data = message["data"]
# If we have more than one PubSub connection, messages may be duplicated
if (
time.time() - ws.last_message_time < 0.1
and ws.last_message_type == message_type
):
ws.last_message_time = time.time()
return
ws.last_message_time = time.time()
ws.last_message_type = message_type
if topic == "community-points-user-v1":
if message_type == "points-earned":
streamer_index = get_streamer_index(ws.streamers, message_data["channel_id"])
earned = message_data["point_gain"]["total_points"]
ws.streamers[streamer_index].channel_points = message_data["balance"]["balance"]
logger.info(
f"🚀 +{earned}{ws.streamers[streamer_index]} - Reason: {message_data['point_gain']['reason_code']}."
)
elif message_type == "claim-available":
streamer_index = get_streamer_index(ws.streamers, message_data["claim"]["channel_id"])
ws.twitch.claim_channel_points_bonus(
ws.streamers[streamer_index], message_data["claim"]["id"]
)
elif topic == "video-playback-by-id":
streamer_index = get_streamer_index(ws.streamers, topic_user)
if message_type == "stream-down":
ws.streamers[streamer_index].set_offline()
elif message_type == "viewcount":
ws.twitch.check_streamer_online(ws.streamers[streamer_index])
# There is stream-up message type, but it's sent earlier than the API updates
elif topic == "raid":
streamer_index = get_streamer_index(ws.streamers, topic_user)
if message_type == "raid_update_v2":
raid = Raid(message["raid"]["id"], message["raid"]["target_login"])
ws.twitch.update_raid(ws.streamers[streamer_index], raid)
elif topic == "predictions-channel-v1":
streamer_index = get_streamer_index(ws.streamers, topic_user) # message_data["event"]["channel_id"]
event_id = message_data["event"]["id"]
event_status = message_data["event"]["status"]
current_timestamp = parser.parse(message_data["timestamp"])
if event_id not in ws.events_predictions:
if event_status == "ACTIVE":
event = EventPrediction(
ws.streamers[streamer_index],
event_id,
parser.parse(message_data["event"]["created_at"]),
float(message_data["event"]["prediction_window_seconds"]) - 20,
event_status,
message_data["event"]["outcomes"]
)
if event.closing_bet_after(current_timestamp) > 0:
ws.events_predictions[event_id] = event
if ws.twitch_browser.currently_is_betting is False:
if ws.twitch_browser.start_bet(ws.events_predictions[event_id]):
complete_bet_thread = threading.Timer(event.closing_bet_after(current_timestamp), ws.twitch_browser.complete_bet, (ws.events_predictions[event_id],))
complete_bet_thread.start()
logger.info(f"⏰ A thread should start and place the bet after: {event.closing_bet_after(current_timestamp)}s for the event: {ws.events_predictions[event_id]}")
else:
ws.events_predictions[event_id].status = event_status
ws.events_predictions[event_id].bet.update_outcomes(message_data["event"]["outcomes"])
elif response["type"] == "RESPONSE" and len(response.get("error", "")) > 0:
raise RuntimeError(f"Error while trying to listen for a topic: {response}")
elif response["type"] == "RECONNECT":
WebSocketsPool.handle_websocket_reconnection(ws)