@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user