Merge pull request #575 from mpforce1/community-goals

Community Goals
This commit is contained in:
Roman Davydov
2024-10-06 13:13:50 +03:00
committed by GitHub
8 changed files with 300 additions and 105 deletions
+11 -9
View File
@@ -272,8 +272,9 @@ twitch_miner = TwitchChannelPointsMiner(
make_predictions=True, # If you want to Bet / Make prediction
follow_raid=True, # Follow raid to obtain more points
claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21
claim_moments=True, # If set to True, https://help.twitch.tv/s/article/moments will be claimed when available
claim_moments=True, # If set to True, https://help.twitch.tv/s/article/moments will be claimed when available
watch_streak=True, # If a streamer go online change the priority of streamers array and catch the watch screak. Issue #11
community_goals=False, # If True, contributes the max channel points per stream to the streamers' community challenge goals
chat=ChatPresence.ONLINE, # Join irc chat to increase watch-time [ALWAYS, NEVER, ONLINE, OFFLINE]
bet=BetSettings(
strategy=Strategy.SMART, # Choose you strategy!
@@ -305,11 +306,11 @@ twitch_miner = TwitchChannelPointsMiner(
twitch_miner.mine(
[
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=True, percentage_gap=20 , max_points=234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.LTE, value=800 ) ) )),
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , stealth_mode=False, percentage_gap=20 , max_points=1234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_POINTS, where=Condition.GTE, value=250 ) ) )),
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=False, percentage_gap=30 , max_points=50000 , filter_condition=FilterCondition(by=OutcomeKeys.ODDS, where=Condition.LT, value=300 ) ) )),
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )),
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , stealth_mode=True, percentage_gap=20 , max_points=90 , filter_condition=FilterCondition(by=OutcomeKeys.PERCENTAGE_USERS, where=Condition.GTE, value=300 ) ) )),
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , community_goals=False , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=True, percentage_gap=20 , max_points=234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.LTE, value=800 ) ) )),
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , stealth_mode=False, percentage_gap=20 , max_points=1234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_POINTS, where=Condition.GTE, value=250 ) ) )),
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , community_goals=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=False, percentage_gap=30 , max_points=50000 , filter_condition=FilterCondition(by=OutcomeKeys.ODDS, where=Condition.LT, value=300 ) ) )),
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True , )),
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , community_goals=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , stealth_mode=True, percentage_gap=20 , max_points=90 , filter_condition=FilterCondition(by=OutcomeKeys.PERCENTAGE_USERS, where=Condition.GTE, value=300 ) ) )),
Streamer("streamer-username06"),
Streamer("streamer-username07"),
Streamer("streamer-username08"),
@@ -527,11 +528,11 @@ Discord(
```
#### Generic Webhook
You can use generic webhook
You can use generic webhook
| Key | Type | Default | Description |
|----------------------- |------------------|-----------|------------------------------------------------------------------- |
| `endpoint` | string | | webhook url |
| `endpoint` | string | | webhook url |
| `method` | string | | `POST` or `GET` |
| `events` | list | | Only these events will be sent to the endpoint. Array of Event. or str |
@@ -573,6 +574,7 @@ Webhook(
| `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you can claim the drops from Twitch Inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) |
| `claim_moments` | bool | True | If set to True, [moments](https://help.twitch.tv/s/article/moments) will be claimed when available |
| `watch_streak` | bool | True | Choose if you want to change a priority for these streamers and try to catch the Watch Streak event [#11](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/11) |
| `community_goals` | bool | False | If True, contributes the max channel points per stream to the streamers' community challenge goals |
| `bet` | BetSettings | | Rules to follow for the bet |
| `chat` | ChatPresence | ONLINE | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) |
@@ -705,7 +707,7 @@ You can also follow this [video tutorial](https://www.youtube.com/watch?v=0VkM7N
## Termux
**1. Upgrade packages**
```
```
pkg upgrade
```
@@ -88,8 +88,7 @@ class TwitchChannelPointsMiner:
):
# Fixes TypeError: 'NoneType' object is not subscriptable
if not username or username == "your-twitch-username":
logger.error(
"Please edit your runner file (usually run.py) and try again.")
logger.error("Please edit your runner file (usually run.py) and try again.")
logger.error("No username, exiting...")
sys.exit(0)
@@ -164,16 +163,14 @@ class TwitchChannelPointsMiner:
logger.info(
f"Twitch Channel Points Miner v2-{current_version} (fork by rdavydov)"
)
logger.info(
"https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2")
logger.info("https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2")
if github_version == "0.0.0":
logger.error(
"Unable to detect if you have the latest version of this script"
)
elif current_version != github_version:
logger.info(
f"You are running version {current_version} of this script")
logger.info(f"You are running version {current_version} of this script")
logger.info(f"The latest version on GitHub is {github_version}")
for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]:
@@ -190,16 +187,19 @@ class TwitchChannelPointsMiner:
if Settings.enable_analytics is True:
from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer
days_ago = days_ago if days_ago <= 365*15 else 365*15
days_ago = days_ago if days_ago <= 365 * 15 else 365 * 15
http_server = AnalyticsServer(
host=host, port=port, refresh=refresh, days_ago=days_ago, username=self.username
host=host,
port=port,
refresh=refresh,
days_ago=days_ago,
username=self.username,
)
http_server.daemon = True
http_server.name = "Analytics Thread"
http_server.start()
else:
logger.error(
"Can't start analytics(), please set enable_analytics=True")
logger.error("Can't start analytics(), please set enable_analytics=True")
def mine(
self,
@@ -245,8 +245,7 @@ class TwitchChannelPointsMiner:
streamers_dict[username] = streamer
if followers is True:
followers_array = self.twitch.get_followers(
order=followers_order)
followers_array = self.twitch.get_followers(order=followers_order)
logger.info(
f"Load {len(followers_array)} followers from your profile!",
extra={"emoji": ":clipboard:"},
@@ -269,8 +268,7 @@ class TwitchChannelPointsMiner:
if isinstance(streamers_dict[username], Streamer) is True
else Streamer(username)
)
streamer.channel_id = self.twitch.get_channel_id(
username)
streamer.channel_id = self.twitch.get_channel_id(username)
streamer.settings = set_default_settings(
streamer.settings, Settings.streamer_settings
)
@@ -312,8 +310,7 @@ class TwitchChannelPointsMiner:
# If we have at least one streamer with settings = claim_drops True
# Spawn a thread for sync inventory and dashboard
if (
at_least_one_value_in_settings_is(
self.streamers, "claim_drops", True)
at_least_one_value_in_settings_is(self.streamers, "claim_drops", True)
is True
):
self.sync_campaigns_thread = threading.Thread(
@@ -372,14 +369,17 @@ class TwitchChannelPointsMiner:
if streamer.settings.make_predictions is True:
self.ws_pool.submit(
PubsubTopic("predictions-channel-v1",
streamer=streamer)
PubsubTopic("predictions-channel-v1", streamer=streamer)
)
if streamer.settings.claim_moments is True:
self.ws_pool.submit(
PubsubTopic("community-moments-channel-v1",
streamer=streamer)
PubsubTopic("community-moments-channel-v1", streamer=streamer)
)
if streamer.settings.community_goals is True:
self.ws_pool.submit(
PubsubTopic("community-points-channel-v1", streamer=streamer)
)
refresh_context = time.time()
@@ -396,8 +396,7 @@ class TwitchChannelPointsMiner:
logger.info(
f"#{index} - The last PING was sent more than 10 minutes ago. Reconnecting to the WebSocket..."
)
WebSocketsPool.handle_reconnection(
self.ws_pool.ws[index])
WebSocketsPool.handle_reconnection(self.ws_pool.ws[index])
if ((time.time() - refresh_context) // 60) >= 30:
refresh_context = time.time()
+138 -63
View File
@@ -11,16 +11,15 @@ import random
import re
import string
import time
from pathlib import Path
from secrets import choice, token_hex
# import json
import requests
import validators
from pathlib import Path
from secrets import choice, token_hex
# from urllib.parse import quote
# from base64 import urlsafe_b64decode
# from datetime import datetime
from TwitchChannelPointsMiner.classes.entities.Campaign import Campaign
from TwitchChannelPointsMiner.classes.entities.CommunityGoal import CommunityGoal
from TwitchChannelPointsMiner.classes.entities.Drop import Drop
from TwitchChannelPointsMiner.classes.Exceptions import (
StreamerDoesNotExistException,
@@ -45,6 +44,11 @@ from TwitchChannelPointsMiner.utils import (
internet_connection_available,
)
# from urllib.parse import quote
# from base64 import urlsafe_b64decode
# from datetime import datetime
logger = logging.getLogger(__name__)
@@ -109,7 +113,7 @@ class Twitch(object):
"player": "site",
"user_id": self.twitch_login.get_user_id(),
"live": True,
"channel": streamer.username
"channel": streamer.username,
}
if (
@@ -133,10 +137,10 @@ class Twitch(object):
# fixes AttributeError: 'NoneType' object has no attribute 'group'
# headers = {"User-Agent": self.user_agent}
from TwitchChannelPointsMiner.constants import USER_AGENTS
headers = {"User-Agent": USER_AGENTS["Linux"]["FIREFOX"]}
main_page_request = requests.get(
streamer.streamer_url, headers=headers)
main_page_request = requests.get(streamer.streamer_url, headers=headers)
response = main_page_request.text
# logger.info(response)
regex_settings = "(https://static.twitchcdn.net/config/settings.*?js|https://assets.twitch.tv/config/settings.*?.js)"
@@ -145,11 +149,9 @@ class Twitch(object):
settings_request = requests.get(settings_url, headers=headers)
response = settings_request.text
regex_spade = '"spade_url":"(.*?)"'
streamer.stream.spade_url = re.search(
regex_spade, response).group(1)
streamer.stream.spade_url = re.search(regex_spade, response).group(1)
except requests.exceptions.RequestException as e:
logger.error(
f"Something went wrong during extraction of 'spade_url': {e}")
logger.error(f"Something went wrong during extraction of 'spade_url': {e}")
def get_broadcast_id(self, streamer):
json_data = copy.deepcopy(GQLOperations.WithIsStreamLiveQuery)
@@ -163,8 +165,7 @@ class Twitch(object):
raise StreamerIsOfflineException
def get_stream_info(self, streamer):
json_data = copy.deepcopy(
GQLOperations.VideoPlayerStreamInfoOverlayChannel)
json_data = copy.deepcopy(GQLOperations.VideoPlayerStreamInfoOverlayChannel)
json_data["variables"] = {"channel": streamer.username}
response = self.post_gql_request(json_data)
if response != {}:
@@ -236,8 +237,7 @@ class Twitch(object):
logger.info(
f"Joining raid from {streamer} to {raid.target_login}!",
extra={"emoji": ":performing_arts:",
"event": Events.JOIN_RAID},
extra={"emoji": ":performing_arts:", "event": Events.JOIN_RAID},
)
def viewer_is_mod(self, streamer):
@@ -394,13 +394,11 @@ class Twitch(object):
streamers_watching += streamers_index[:2]
elif (
prior in [Priority.POINTS_ASCENDING,
Priority.POINTS_DESCENDING]
prior in [Priority.POINTS_ASCENDING, Priority.POINTS_DESCENDING]
and len(streamers_watching) < 2
):
items = [
{"points": streamers[index].channel_points,
"index": index}
{"points": streamers[index].channel_points, "index": index}
for index in streamers_index
]
items = sorted(
@@ -410,8 +408,7 @@ class Twitch(object):
True if prior == Priority.POINTS_DESCENDING else False
),
)
streamers_watching += [item["index"]
for item in items][:2]
streamers_watching += [item["index"] for item in items][:2]
elif prior == Priority.STREAK and len(streamers_watching) < 2:
"""
@@ -427,8 +424,7 @@ class Twitch(object):
and (
streamers[index].offline_at == 0
or (
(time.time() -
streamers[index].offline_at)
(time.time() - streamers[index].offline_at)
// 60
)
> 30
@@ -455,8 +451,7 @@ class Twitch(object):
]
streamers_with_multiplier = sorted(
streamers_with_multiplier,
key=lambda x: streamers[x].total_points_multiplier(
),
key=lambda x: streamers[x].total_points_multiplier(),
reverse=True,
)
streamers_watching += streamers_with_multiplier[:2]
@@ -475,25 +470,27 @@ class Twitch(object):
####################################
# Start of fix for 2024/5 API Change
# Create the JSON data for the GraphQL request
json_data = copy.deepcopy(
GQLOperations.PlaybackAccessToken)
json_data = copy.deepcopy(GQLOperations.PlaybackAccessToken)
json_data["variables"] = {
"login": streamers[index].username,
"isLive": True,
"isVod": False,
"vodID": "",
# "playerType": "site"
"playerType": "picture-by-picture"
"playerType": "picture-by-picture",
}
# Get signature and value using the post_gql_request method
responsePlaybackAccessToken = self.post_gql_request(
json_data)
responsePlaybackAccessToken = self.post_gql_request(json_data)
logger.debug(
f"Sent PlaybackAccessToken request for {streamers[index]}"
)
signature = responsePlaybackAccessToken["data"]['streamPlaybackAccessToken']["signature"]
value = responsePlaybackAccessToken["data"]['streamPlaybackAccessToken']["value"]
signature = responsePlaybackAccessToken["data"][
"streamPlaybackAccessToken"
]["signature"]
value = responsePlaybackAccessToken["data"][
"streamPlaybackAccessToken"
]["value"]
if not signature or not value:
continue
@@ -503,8 +500,11 @@ class Twitch(object):
RequestBroadcastQualitiesURL = f"https://usher.ttvnw.net/api/channel/hls/{streamers[index].username}.m3u8?sig={signature}&token={value}"
# Get list of video qualities
responseBroadcastQualities = requests.get(RequestBroadcastQualitiesURL, headers={
"User-Agent": self.user_agent}, timeout=20) # timeout=60
responseBroadcastQualities = requests.get(
RequestBroadcastQualitiesURL,
headers={"User-Agent": self.user_agent},
timeout=20,
) # timeout=60
logger.debug(
f"Send RequestBroadcastQualitiesURL request for {streamers[index]} - Status code: {responseBroadcastQualities.status_code}"
)
@@ -513,14 +513,16 @@ class Twitch(object):
BroadcastQualities = responseBroadcastQualities.text
# Just takes the last line, which should be the URL for the lowest quality
BroadcastLowestQualityURL = BroadcastQualities.split(
"\n")[-1]
BroadcastLowestQualityURL = BroadcastQualities.split("\n")[-1]
if not validators.url(BroadcastLowestQualityURL):
continue
# Get list of video URLs
responseStreamURLList = requests.get(BroadcastLowestQualityURL, headers={
"User-Agent": self.user_agent}, timeout=20) # timeout=60
responseStreamURLList = requests.get(
BroadcastLowestQualityURL,
headers={"User-Agent": self.user_agent},
timeout=20,
) # timeout=60
logger.debug(
f"Send BroadcastLowestQualityURL request for {streamers[index]} - Status code: {responseStreamURLList.status_code}"
)
@@ -534,8 +536,11 @@ class Twitch(object):
continue
# Perform a HEAD request to simulate watching the stream
responseStreamLowestQualityURL = requests.head(StreamLowestQualityURL, headers={
"User-Agent": self.user_agent}, timeout=20) # timeout=60
responseStreamLowestQualityURL = requests.head(
StreamLowestQualityURL,
headers={"User-Agent": self.user_agent},
timeout=20,
) # timeout=60
logger.debug(
f"Send StreamLowestQualityURL request for {streamers[index]} - Status code: {responseStreamLowestQualityURL.status_code}"
)
@@ -611,12 +616,10 @@ class Twitch(object):
)
except requests.exceptions.ConnectionError as e:
logger.error(
f"Error while trying to send minute watched: {e}")
logger.error(f"Error while trying to send minute watched: {e}")
self.__check_connection_handler(chunk_size)
except requests.exceptions.Timeout as e:
logger.error(
f"Error while trying to send minute watched: {e}")
logger.error(f"Error while trying to send minute watched: {e}")
self.__chuncked_sleep(
next_iteration - time.time(), chunk_size=chunk_size
@@ -626,8 +629,7 @@ class Twitch(object):
# self.__chuncked_sleep(60, chunk_size=chunk_size)
self.__chuncked_sleep(20, chunk_size=chunk_size)
except Exception:
logger.error(
"Exception raised in send minute watched", exc_info=True)
logger.error("Exception raised in send minute watched", exc_info=True)
# === CHANNEL POINTS / PREDICTION === #
# Load the amount of current points for a channel, check if a bonus is available
@@ -644,9 +646,17 @@ class Twitch(object):
streamer.channel_points = community_points["balance"]
streamer.activeMultipliers = community_points["activeMultipliers"]
if streamer.settings.community_goals is True:
streamer.community_goals = {
goal["id"]: CommunityGoal.from_gql(goal)
for goal in channel["communityPointsSettings"]["goals"]
}
if community_points["availableClaim"] is not None:
self.claim_bonus(
streamer, community_points["availableClaim"]["id"])
self.claim_bonus(streamer, community_points["availableClaim"]["id"])
if streamer.settings.community_goals is True:
self.contribute_to_community_goals(streamer)
def make_predictions(self, event):
decision = event.bet.calculate(event.streamer.channel_points)
@@ -746,20 +756,16 @@ class Twitch(object):
if Settings.logger.less is False:
logger.info(
f"Claiming the moment for {streamer}!",
extra={"emoji": ":video_camera:",
"event": Events.MOMENT_CLAIM},
extra={"emoji": ":video_camera:", "event": Events.MOMENT_CLAIM},
)
json_data = copy.deepcopy(GQLOperations.CommunityMomentCallout_Claim)
json_data["variables"] = {
"input": {"momentID": moment_id}
}
json_data["variables"] = {"input": {"momentID": moment_id}}
self.post_gql_request(json_data)
# === CAMPAIGNS / DROPS / INVENTORY === #
def __get_campaign_ids_from_streamer(self, streamer):
json_data = copy.deepcopy(
GQLOperations.DropsHighlightService_AvailableDrops)
json_data = copy.deepcopy(GQLOperations.DropsHighlightService_AvailableDrops)
json_data["variables"] = {"channelID": streamer.channel_id}
response = self.post_gql_request(json_data)
try:
@@ -788,8 +794,9 @@ class Twitch(object):
campaigns = response["data"]["currentUser"]["dropCampaigns"] or []
if status is not None:
campaigns = list(
filter(lambda x: x["status"] == status.upper(), campaigns)) or []
campaigns = (
list(filter(lambda x: x["status"] == status.upper(), campaigns)) or []
)
return campaigns
@@ -799,8 +806,7 @@ class Twitch(object):
for chunk in chunks:
json_data = []
for campaign in chunk:
json_data.append(copy.deepcopy(
GQLOperations.DropCampaignDetails))
json_data.append(copy.deepcopy(GQLOperations.DropCampaignDetails))
json_data[-1]["variables"] = {
"dropID": campaign["id"],
"channelLogin": f"{self.twitch_login.get_user_id()}",
@@ -842,8 +848,7 @@ class Twitch(object):
)
json_data = copy.deepcopy(GQLOperations.DropsPage_ClaimDropRewards)
json_data["variables"] = {
"input": {"dropInstanceID": drop.drop_instance_id}}
json_data["variables"] = {"input": {"dropInstanceID": drop.drop_instance_id}}
response = self.post_gql_request(json_data)
try:
# response["data"]["claimDropRewards"] can be null and respose["data"]["errors"] != []
@@ -884,7 +889,6 @@ class Twitch(object):
if (
campaigns_update == 0
# or ((time.time() - campaigns_update) / 60) > 60
# TEMPORARY AUTO DROP CLAIMING FIX
# 30 minutes instead of 60 minutes
or ((time.time() - campaigns_update) / 30) > 30
@@ -938,3 +942,74 @@ class Twitch(object):
self.__check_connection_handler(chunk_size)
self.__chuncked_sleep(60, chunk_size=chunk_size)
def contribute_to_community_goals(self, streamer):
# Don't bother doing the request if no goal is currently started or in stock
if any(
goal.status == "STARTED" and goal.is_in_stock
for goal in streamer.community_goals.values()
):
json_data = copy.deepcopy(GQLOperations.UserPointsContribution)
json_data["variables"] = {"channelLogin": streamer.username}
response = self.post_gql_request(json_data)
user_goal_contributions = response["data"]["user"]["channel"]["self"][
"communityPoints"
]["goalContributions"]
logger.debug(
f"Found {len(user_goal_contributions)} community goals for the current stream"
)
for goal_contribution in user_goal_contributions:
goal_id = goal_contribution["goal"]["id"]
goal = streamer.community_goals[goal_id]
if goal is None:
# TODO should this trigger a new load context request
logger.error(
f"Unable to find context data for community goal {goal_id}"
)
else:
user_stream_contribution = goal_contribution[
"userPointsContributedThisStream"
]
user_left_to_contribute = (
goal.per_stream_user_maximum_contribution
- user_stream_contribution
)
amount = min(
goal.amount_left(),
user_left_to_contribute,
streamer.channel_points,
)
if amount > 0:
self.contribute_to_community_goal(
streamer, goal_id, goal.title, amount
)
else:
logger.debug(
f"Not contributing to community goal {goal.title}, user channel points {streamer.channel_points}, user stream contribution {user_stream_contribution}, all users total contribution {goal.points_contributed}"
)
def contribute_to_community_goal(self, streamer, goal_id, title, amount):
json_data = copy.deepcopy(GQLOperations.ContributeCommunityPointsCommunityGoal)
json_data["variables"] = {
"input": {
"amount": amount,
"channelID": streamer.channel_id,
"goalID": goal_id,
"transactionID": token_hex(16),
}
}
response = self.post_gql_request(json_data)
error = response["data"]["contributeCommunityPointsCommunityGoal"]["error"]
if error:
logger.error(
f"Unable to contribute channel points to community goal '{title}', reason '{error}'"
)
else:
logger.info(
f"Contributed {amount} channel points to community goal '{title}'"
)
streamer.channel_points -= amount
@@ -8,6 +8,7 @@ from threading import Thread, Timer
from dateutil import parser
from TwitchChannelPointsMiner.classes.entities.CommunityGoal import CommunityGoal
from TwitchChannelPointsMiner.classes.entities.EventPrediction import EventPrediction
from TwitchChannelPointsMiner.classes.entities.Message import Message
from TwitchChannelPointsMiner.classes.entities.Raid import Raid
@@ -399,6 +400,25 @@ class WebSocketsPool:
"PREDICTION_MADE",
f"Decision: {event_prediction.bet.decision['choice']} - {event_prediction.title}",
)
elif message.topic == "community-points-channel-v1":
if message.type == "community-goal-created":
# TODO Untested, hard to find this happening live
ws.streamers[streamer_index].add_community_goal(
CommunityGoal.from_pubsub(message.data["community_goal"])
)
elif message.type == "community-goal-updated":
ws.streamers[streamer_index].update_community_goal(
CommunityGoal.from_pubsub(message.data["community_goal"])
)
elif message.type == "community-goal-deleted":
# TODO Untested, not sure what the message format for this is,
# https://github.com/sammwyy/twitch-ps/blob/master/main.js#L417
# suggests that it should be just the entire, now deleted, goal model
ws.streamers[streamer_index].delete_community_goal(message.data["community_goal"]["id"])
if message.type in ["community-goal-updated", "community-goal-created"]:
ws.twitch.contribute_to_community_goals(ws.streamers[streamer_index])
except Exception:
logger.error(
f"Exception raised for topic: {message.topic} and message: {message}",
@@ -0,0 +1,64 @@
class CommunityGoal(object):
__slots__ = [
"goal_id",
"title",
"is_in_stock",
"points_contributed",
"amount_needed",
"per_stream_user_maximum_contribution",
"status"
]
def __init__(
self,
goal_id,
title,
is_in_stock,
points_contributed,
amount_needed,
per_stream_user_maximum_contribution,
status
):
self.goal_id = goal_id
self.title = title
self.is_in_stock = is_in_stock
self.points_contributed = points_contributed
self.amount_needed = amount_needed
self.per_stream_user_maximum_contribution = per_stream_user_maximum_contribution
self.status = status
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.goal_id == other.goal_id
else:
return False
def __repr__(self) -> str:
return f"CommunityGoal(goal_id: {self.goal_id}, title: {self.title}, is_in_stock: {self.is_in_stock}, points_contributed: {self.points_contributed}, amount_needed: {self.amount_needed}, per_stream_user_maximum_contribution: {self.per_stream_user_maximum_contribution}, status: {self.status})"
def amount_left(self):
return self.amount_needed - self.points_contributed
@classmethod
def from_gql(cls, gql_goal):
return cls(
gql_goal["id"],
gql_goal["title"],
gql_goal["isInStock"],
gql_goal["pointsContributed"],
gql_goal["amountNeeded"],
gql_goal["perStreamUserMaximumContribution"],
gql_goal["status"]
)
@classmethod
def from_pubsub(cls, pubsub_goal):
return cls(
pubsub_goal["id"],
pubsub_goal["title"],
pubsub_goal["is_in_stock"],
pubsub_goal["points_contributed"],
pubsub_goal["goal_amount"],
pubsub_goal["per_stream_maximum_user_contribution"],
pubsub_goal["status"]
)
@@ -22,6 +22,7 @@ class StreamerSettings(object):
"claim_drops",
"claim_moments",
"watch_streak",
"community_goals",
"bet",
"chat",
]
@@ -33,6 +34,7 @@ class StreamerSettings(object):
claim_drops: bool = None,
claim_moments: bool = None,
watch_streak: bool = None,
community_goals: bool = None,
bet: BetSettings = None,
chat: ChatPresence = None,
):
@@ -41,6 +43,7 @@ class StreamerSettings(object):
self.claim_drops = claim_drops
self.claim_moments = claim_moments
self.watch_streak = watch_streak
self.community_goals = community_goals
self.bet = bet
self.chat = chat
@@ -54,13 +57,15 @@ class StreamerSettings(object):
]:
if getattr(self, name) is None:
setattr(self, name, True)
if self.community_goals is None:
self.community_goals = False
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})"
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}, community_goals={self.community_goals}, bet={self.bet}, chat={self.chat})"
class Streamer(object):
@@ -73,6 +78,7 @@ class Streamer(object):
"online_at",
"offline_at",
"channel_points",
"community_goals",
"minute_watched_requests",
"viewer_is_mod",
"activeMultipliers",
@@ -93,6 +99,7 @@ class Streamer(object):
self.online_at = 0
self.offline_at = 0
self.channel_points = 0
self.community_goals = {}
self.minute_watched_requests = None
self.viewer_is_mod = False
self.activeMultipliers = None
@@ -211,7 +218,11 @@ class Streamer(object):
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"))
else (
"#ffe045"
if event_type == "PREDICTION_MADE"
else ("#36b535" if event_type == "WIN" else "#ff4545")
)
)
data = {
"borderColor": primary_color,
@@ -236,13 +247,12 @@ class Streamer(object):
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
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 {}
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)
@@ -282,3 +292,9 @@ class Streamer(object):
self.leave_chat()
elif self.settings.chat == ChatPresence.OFFLINE:
self.__join_chat()
def update_community_goal(self, community_goal):
self.community_goals[community_goal.goal_id] = community_goal
def delete_community_goal(self, goal_id):
self.community_goals.pop(goal_id)
+19 -1
View File
@@ -5,7 +5,7 @@ URL = "https://www.twitch.tv" # Browser, Apps
IRC = "irc.chat.twitch.tv"
IRC_PORT = 6667
WEBSOCKET = "wss://pubsub-edge.twitch.tv/v1"
CLIENT_ID = "ue6666qo983tsx6so1t0vnawi233wa" # TV
CLIENT_ID = "ue6666qo983tsx6so1t0vnawi233wa" # TV
# CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko" # Browser
# CLIENT_ID = "r8s4dac0uhzifbpu9sjdiwzctle17ff" # Mobile Browser
# CLIENT_ID = "kd1unb4b3q4t58fwlpcbzcbnm76a8fp" # Android App
@@ -209,3 +209,21 @@ class GQLOperations:
}
},
}
UserPointsContribution = {
"operationName": "UserPointsContribution",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "23ff2c2d60708379131178742327ead913b93b1bd6f665517a6d9085b73f661f"
}
}
}
ContributeCommunityPointsCommunityGoal = {
"operationName": "ContributeCommunityPointsCommunityGoal",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "5774f0ea5d89587d73021a2e03c3c44777d903840c608754a1be519f51e37bb6"
}
}
}
+6 -5
View File
@@ -87,6 +87,7 @@ twitch_miner = TwitchChannelPointsMiner(
claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21
claim_moments=True, # If set to True, https://help.twitch.tv/s/article/moments will be claimed when available
watch_streak=True, # If a streamer go online change the priority of streamers array and catch the watch screak. Issue #11
community_goals=False, # If True, contributes the max channel points per stream to the streamers' community challenge goals
chat=ChatPresence.ONLINE, # Join irc chat to increase watch-time [ALWAYS, NEVER, ONLINE, OFFLINE]
bet=BetSettings(
strategy=Strategy.SMART, # Choose you strategy!
@@ -118,11 +119,11 @@ twitch_miner = TwitchChannelPointsMiner(
twitch_miner.mine(
[
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=True, percentage_gap=20 , max_points=234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.LTE, value=800 ) ) )),
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , stealth_mode=False, percentage_gap=20 , max_points=1234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_POINTS, where=Condition.GTE, value=250 ) ) )),
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=False, percentage_gap=30 , max_points=50000 , filter_condition=FilterCondition(by=OutcomeKeys.ODDS, where=Condition.LT, value=300 ) ) )),
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )),
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , stealth_mode=True, percentage_gap=20 , max_points=90 , filter_condition=FilterCondition(by=OutcomeKeys.PERCENTAGE_USERS, where=Condition.GTE, value=300 ) ) )),
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , community_goals=False , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=True, percentage_gap=20 , max_points=234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.LTE, value=800 ) ) )),
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , stealth_mode=False, percentage_gap=20 , max_points=1234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_POINTS, where=Condition.GTE, value=250 ) ) )),
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , community_goals=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=False, percentage_gap=30 , max_points=50000 , filter_condition=FilterCondition(by=OutcomeKeys.ODDS, where=Condition.LT, value=300 ) ) )),
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True , )),
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , community_goals=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , stealth_mode=True, percentage_gap=20 , max_points=90 , filter_condition=FilterCondition(by=OutcomeKeys.PERCENTAGE_USERS, where=Condition.GTE, value=300 ) ) )),
Streamer("streamer-username06"),
Streamer("streamer-username07"),
Streamer("streamer-username08"),