Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9f47e5e
2.0.0 Release
victorblino Sep 29, 2022
c6cea52
Fix: emojis variables
victorblino Sep 29, 2022
673d60e
Fix: typo error in channel_update function
victorblino Sep 29, 2022
c68344c
Fix: typo error file name
victorblino Sep 29, 2022
6e58d25
add: github actions (pyright)
victorblino Sep 29, 2022
5d9a993
add: print events in event sub functions
victorblino Sep 29, 2022
ab5b020
Update twitchFunctions.py
victorblino Sep 29, 2022
250f1c6
minor fix
victorblino Sep 29, 2022
862e53d
Fix: typo error
victorblino Sep 29, 2022
a53fdaf
add: async in twitch functions
victorblino Sep 29, 2022
c66185a
Update twitchFunctions.py
victorblino Sep 29, 2022
9175167
Update twitchFunctions.py
victorblino Sep 29, 2022
d07462c
Typo error
victorblino Sep 29, 2022
a3d083d
Fix: bugs in many functions
victorblino Sep 30, 2022
e610268
fix: set variable 'online' when stream is online and offline
victorblino Sep 30, 2022
5c94ae5
fix: dont post the game image if it is in games_blacklist
victorblino Sep 30, 2022
0ad6fec
fix: typo error in twitter functions
victorblino Sep 30, 2022
1ad03e9
fix: typo error in events
victorblino Sep 30, 2022
fbe146a
fix: fix stream_online function
victorblino Sep 30, 2022
cec13d5
fix: append game played
victorblino Oct 1, 2022
877db6f
fix: title stream
victorblino Oct 1, 2022
45c7d62
fix: games not append
victorblino Oct 2, 2022
ae84124
remove workflows temporarly
victorblino Oct 2, 2022
ce56045
Fix: bug not append
victorblino Oct 6, 2022
608511b
fix: link twitch tracker
victorblino Oct 7, 2022
777e264
fix: stream_offline function
victorblino Nov 4, 2022
dd3b1df
Fix Temporário
victorblino Nov 23, 2022
9e786a7
Fix LinkTwitchTracker
victorblino Nov 26, 2022
5c4c69e
Fix Twitch Tracker Function
victorblino Dec 2, 2022
8922fcd
Fix Variables games_blacklist
victorblino Dec 3, 2022
e791580
fix
victorblino Dec 26, 2022
a74d7bb
Fix Bug Twitch Tracker Link
victorblino Jan 31, 2023
b6eab34
Update except error in live on
victorblino Jan 31, 2023
0674eec
Fix LinkTwitchTracker
victorblino Feb 2, 2023
d6e2a02
Update Trip -> Bug Twitch Tracker "Fixed"
victorblino Mar 14, 2023
a69230f
fix bug vod timestamp
victorblino May 1, 2023
bd654e0
Revert "Update Trip -> Bug Twitch Tracker "Fixed""
victorblino May 4, 2023
2ee5b36
Revert "fix bug vod timestamp"
victorblino May 4, 2023
cf45dd9
Update Twitter V2 API
victorblino Jun 30, 2023
390d98d
maybe?
victorblino Jun 30, 2023
a20aae7
Solve Lib
victorblino Jun 30, 2023
c4df013
Commit Fix Twitter Post
victorblino Jul 6, 2023
1ae6f7d
Fix Connect Twitter
victorblino Jul 6, 2023
1e91a98
remove timestamp
victorblino Jul 6, 2023
8622c93
Fix Tweet is Same
victorblino Aug 3, 2023
67c3139
Modernize dependencies and clean up Twitter/Twitch integration
victorblino Mar 7, 2026
19c009d
Add retry logic for TwitterServerError 503 in postTweet
victorblino Mar 7, 2026
8f92830
Handle BadRequest (duplicate tweet) and TooManyRequests in postTweet
victorblino Mar 7, 2026
7ad0b99
Upgrade twitchAPI 2.5.7.1 → 4.5.0 with async rewrite
victorblino Mar 8, 2026
1dd02de
fix: fix change title if stream is online
victorblino May 10, 2026
63082c8
Add StreamState, tests, and BotConfig loader
victorblino Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
WEBHOOK_URL=
# Twitch
TWITCH_APP_ID=
TWITCH_APP_SECRET=
TWITCH_BEARER=
TARGET_USERNAME=

# Webhook (URL pública da VPS para receber eventos da Twitch)
WEBHOOK_URL=
PORT=8080

# Twitter/X
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=

TARGET_USERNAME=

LOGGING=
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
gameImg.jpg
imageGame.jpg

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down Expand Up @@ -131,4 +131,5 @@ dmypy.json
.gitpod.yml


test.py
test.py
.vscode/
167 changes: 16 additions & 151 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,151 +1,16 @@
import os
import tweepy
import logging
import threading
from twitchAPI import Twitch, EventSub
from dotenv import load_dotenv
from functions.functionsBot import compareImages
from functions.twitchAPI import getStream, dateStream, getVideo
from asyncio import sleep

# load env variables
load_dotenv()

# env variables
WEBHOOK_URL = os.environ.get('WEBHOOK_URL')
APP_ID = os.environ.get('TWITCH_APP_ID')
APP_SECRET = os.environ.get('TWITCH_APP_SECRET')
ACCESS_TOKEN = os.environ.get('TWITTER_ACCESS_TOKEN')
ACCESS_SECRET = os.environ.get('TWITTER_ACCESS_SECRET')
CONSUMER_KEY = os.environ.get('TWITTER_CONSUMER_KEY')
CONSUMER_SECRET = os.environ.get('TWITTER_CONSUMER_SECRET')
PORT = os.environ.get('PORT', 8080)
TARGET_USERNAME = os.environ.get('TARGET_USERNAME')
LOGGING = os.environ.get('LOGGING')

if LOGGING == 'TRUE':
logging.basicConfig(level=logging.INFO)

# global variables
currentGame = None
currentTitle = None
streamId = None
online = False
gamesPlayed = list()
gamesBlacklist = ('Just Chatting', 'Watch Parties', 'Tabletop RPGs')
forever = threading.Event()

# login in twitch api
twitch = Twitch(APP_ID, APP_SECRET)
twitch.authenticate_app([])

# twitter authentication
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)
api = tweepy.API(auth)

try:
api.verify_credentials()
print('Authentication Successful')
except:
print('Authentication Error')

# get the user_id from twitch user
uid = twitch.get_users(logins=[TARGET_USERNAME])
user_id = uid['data'][0]['id']
currentTitle = twitch.get_channel_information(user_id)['data'][0]['title']

# get the informations
try:
stream = twitch.get_streams(user_id=user_id)
currentGame = stream['data'][0]['game_name']
currentTitle = stream['data'][0]['title']
streamId = stream['data'][0]['id']
if currentGame not in gamesBlacklist:
gamesPlayed.append(currentGame)
online = True
except:
online = False

# functions callbacks
async def stream_online(data: dict):
global online, currentGame, streamId
stream = twitch.get_streams(user_id=user_id)
title = stream['data'][0]['title']
currentGame = stream['data'][0]['game_name']
streamId = stream['data'][0]['id']
api.update_status(f'Cellbit entrou ao vivo!\n\nTítulo: {title}\ntwitch.tv/cellbit')
online = True

async def stream_offline(data: dict):
global online, gamesPlayed
api.update_status('Cellbit encerrou a live!')
online = False

if len(gamesPlayed) > 0:
tweetId = api.user_timeline(screen_name='livesdocellbit')[0].id

date = dateStream()
status = f"[{date['day']}/{date['month']}/{date['year']}] Games Jogados:\n\n"
for game in gamesPlayed:
status += f'• {game}\n'
if getVideo['title'] != currentTitle:
status += f'\nVOD: https://twitchtracker.com/cellbit/streams/{streamId}'
else:
status += f'\nVOD: {getVideo()["link"]}'
api.update_status(status, in_reply_to_status_id = tweetId)
gamesPlayed = list()

async def channel_update(data: dict):
global currentGame, currentTitle, gamesPlayed

game = data['event']['category_name']
title = data['event']['title']

if game != currentGame and online == True:
# stream = twitch.get_streams(user_id=user_id)
timeVod = getStream()
h, m, s = timeVod['vodHours'], timeVod['vodMinutes'], timeVod['vodSeconds']
try:
import urllib.request
imageUrl = twitch.get_games(names=game)['data'][0]['box_art_url'].replace('{width}', '600').replace('{height}', '800')
urllib.request.urlretrieve(imageUrl, 'gameImg.jpg')
status = f'Cellbit está jogando: {game}\nTempo no VOD: {h}h{m}m{s}s\n\ntwitch.tv/cellbit'
if compareImages() or game == 'Just Chatting':
api.update_status(status)
else:
api.update_status_with_media(status, 'gameImg.jpg')
except:
api.update_status(status)
finally:
if game not in gamesPlayed and game not in gamesBlacklist:
gamesPlayed.append(game)
currentGame = game

# sleep(1)
# infoVideo = getVideo()
if title != currentTitle and online == False:
api.update_status(f'[TÍTULO] {title}')
currentTitle = title

# subscribe to EventSub
hook = EventSub(WEBHOOK_URL, APP_ID, PORT, twitch)
hook.unsubscribe_all()
hook.start()

print('Iniciando webhooks...\n')

hook.listen_channel_update(user_id, channel_update)
print('[OK] CHANNEL UPDATE - WEBHOOK')
hook.listen_stream_offline(user_id, stream_offline)
print('[OK] STREAM OFFLINE - WEBHOOK')
hook.listen_stream_online(user_id, stream_online)
print('[OK] STREAM ONLINE - WEBHOOK')
print('\n')


try:
print('Rodando!')
forever.wait()
finally:
hook.stop()
import asyncio
from functions.botFunctions import printEvent
from functions.twitchFunctions import connectEventSub, connectTwitch, verifyStreamIsOnline

async def main():
await connectTwitch()
hook = await connectEventSub()
await verifyStreamIsOnline()

printEvent(True, 'on_ready')
try:
await asyncio.Event().wait()
finally:
await hook.stop()

asyncio.run(main())
31 changes: 31 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from dataclasses import dataclass
import os

@dataclass(frozen=True)
class BotConfig:
twitch_app_id: str
twitch_app_secret: str
webhook_url: str
twitter_consumer_key: str
twitter_consumer_secret: str
twitter_access_token: str
twitter_access_token_secret: str
streamer_username: str

@staticmethod
def load_from_env() -> 'BotConfig':
def get(name: str, required: bool = True) -> str:
value = os.getenv(name)
if required and not value:
raise ValueError(f"Environment variable '{name}' is required but not set.")
return value or ""
return BotConfig(
twitch_app_id=get('TWITCH_APP_ID'),
twitch_app_secret=get('TWITCH_APP_SECRET'),
webhook_url=get('WEBHOOK_URL'),
twitter_consumer_key=get('TWITTER_CONSUMER_KEY'),
twitter_consumer_secret=get('TWITTER_CONSUMER_SECRET'),
twitter_access_token=get('TWITTER_ACCESS_TOKEN'),
twitter_access_token_secret=get('TWITTER_ACCESS_TOKEN_SECRET'),
streamer_username=get('STREAMER_USERNAME')
)
23 changes: 23 additions & 0 deletions core/stream_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass

@dataclass
class StreamState:
"""
Represents the current state of the stream, including whether it's online, the current game being played, and the stream title.
"""

online: bool = False
current_game: str = ""
stream_title: str = ""

def reset_for_offline(self):
"""
Resets the stream state to default values when the stream goes offline.
"""
self.online = False

def update_from_update(self, update: dict):
self.online = update.get('online', self.online)
self.current_game = update.get('current_game', self.current_game)
self.stream_title = update.get('stream_title', self.stream_title)

63 changes: 63 additions & 0 deletions functions/botFunctions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from utils import variables

def printEvent(sucessOrfail: bool, event: str, info: str='') -> None:
colors = {
True: '\33[92m',
False: '\33[91m'
}

events = {
# Bot

'info_stream': '[BOT] Informações da Stream',
'on_ready': '[BOT] ONLINE',

# Twitch Authentication
'twitch_authenticated': '[TWITCH AUTHENTICATION] OK',

# Twitter Authenticaton
'twitter_authenticated': '[TWITTER AUTHENTICATION] OK',

# Twitter Posts
'twitter_post': '[TWITTER] POST SUCESSO',
'twitter_post_image': '[TWITTER] POST IMAGE SUCESSO',
'twitter_post_reply': '[TWITTER] POST REPLY SUCESSO',

# Twitch Subscribe Event Subs
'event_sub': '[EVENTSUB] OK',

# Twitch Event Sub
'live_on': '[TWITCH EVENTSUB] - Live Online!',
'live_off': '[TWITCH EVENTSUB] - Live Offline',
'game_changed': '[TWITCH EVENTSUB] - Game trocado - ',
'title': '[TWITCH EVENTSUB] - Título trocado - '

}

return print(colors[sucessOrfail], events[event])

def downloadImageGame(image_url):
from urllib.request import urlretrieve
try:
urlretrieve(image_url, 'imageGame.jpg')
except:
return False

def compareImages():
from base64 import b64encode
with open('404.jpg', 'rb') as not_found:
image_not_found = b64encode(not_found.read())
with open ('imageGame.jpg', 'rb') as game_image:
image_game = b64encode(game_image.read())
if image_not_found == image_game:
return True
return False

def gamesPlayed(games_list):
games = ''
for game in games_list:
games = f'• {game}\n'
return games

def linkTwitchTracker(stream_id):
return f'https://twitchtracker.com/{variables.streamer_nickname}/streams/{str(stream_id)}'
11 changes: 0 additions & 11 deletions functions/functionsBot.py

This file was deleted.

Loading