├── README.md ├── bdb.py ├── botter.py ├── chat_count.py ├── get_exceptions.py.example ├── get_passwords.py.example ├── global_consts.py ├── handle_twitter.py ├── pass_info.py.example ├── requirements.txt ├── twitch_chatters.py ├── twitch_viewers.py └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | ###How this works### 2 | Essentially this is a bot that tries to detect streams with fake viewers via 3 | the Twitch API and the power of statistics. 4 | 5 | File layout (primarily): 6 | 7 | bdb.py -> twitch_chatters -> handle_twitter 8 | | 9 | v 10 | twitch_viewers 11 | 12 | Primarily, it looks at certain metrics such as the ratio between chatters to reported viewers. 13 | More bot detection methods are listed below or found in the source code (or the dev branch 14 | (which may or may not be 100% up to date)). 15 | Let me know if you see any other patterns in streams with fake viewers that i'm not taking 16 | into account by [emailing me](mailto:popcorncolonel@gmail.com), 17 | or feel free to contact me with questions/anything at that email address. 18 | 19 | I'm not necessarily accusing the streamers of botting their own channels. 20 | I'm not affiliated with Twitch. 21 | 22 | ###Usage### 23 | "python bdb.py" 24 | 25 | Note: To run it locally (not send out tweets), set "tweetmode" to False in 26 | global_consts.py. 27 | Modules needed: 28 | 29 | * [Requests](http://docs.python-requests.org/en/latest/) 30 | 31 | **If tweetmode**, 32 | * [Twython](http://twython.readthedocs.org/en/latest/) 33 | * [Python-Twitter](http://code.google.com/p/python-twitter/) 34 | 35 | ###Notes### 36 | * Some other detection methods: 37 | * Take into account the average chat viewer ratio **for each game** vs per 38 | user: maybe a bad idea - skewed by bots if a low number of streamers for 39 | a certain game 40 | * Long-term database storage of average viewer count 41 | * If the stream has more viewers than followers, there may be cause for concern. 42 | * More chatters than viewers, plus weird names/inactive chat => suspicion 43 | * Sharp, significant increases in viewers without proportional increases in 44 | the number of chat users (take into account videos in the front page of 45 | Twitch.. maybe check the front page of Twitch to see which user is there? 46 | On average how many users are watching via the main twitch page?) 47 | 48 | * New heuristics for detecting chatbots: very low average follower count in 49 | chat (regarding how many people each user is following) is indicative of 50 | chatbots. Also if a lot of the bots have the exact same number (say, under 5). 51 | Or, if most of the followers are following the exact same streams. 52 | The main problems with these is that I need to individually access the 53 | Twitch servers for each chatter; this could take hours for certain streams 54 | with large viewer counts. 55 | 56 | ###TL;DR### 57 | Go to http://www.twitter.com/BotDetectorBot! 58 | Program usage: "python bdb.py" 59 | 60 | -------------------------------------------------------------------------------- /bdb.py: -------------------------------------------------------------------------------- 1 | from twitch_chatters import search_all_games, remove_offline 2 | 3 | while True: 4 | search_all_games() 5 | remove_offline() 6 | 7 | -------------------------------------------------------------------------------- /botter.py: -------------------------------------------------------------------------------- 1 | class Botter(object): 2 | def __init__(self, user, game, ratio, chatters, viewers): 3 | self.user = user #str 4 | self.game = game #str 5 | self.ratio = ratio #float 6 | self.chatters = chatters #int 7 | self.viewers = viewers #int 8 | 9 | -------------------------------------------------------------------------------- /chat_count.py: -------------------------------------------------------------------------------- 1 | #counts the number of chatters in a certain Twitch chat room. 2 | import sys 3 | import socket 4 | import requests 5 | from pass_info import get_username, get_password 6 | from twitch_viewers import remove_non_ascii 7 | 8 | names_num = "353" 9 | end_names_num = "366" 10 | 11 | port1 = 6667 #irc 12 | port2 = 80 #http 13 | port3 = 443 #https 14 | default_port = port3 15 | 16 | i = 0# 17 | def count_users(full_msg): 18 | data = full_msg.split("\r\n") 19 | count = 0 20 | for namegroup in data: 21 | if ("End of /NAMES list" in namegroup or 22 | "tmi.twitch.tv " + end_names_num in namegroup): 23 | if (count == 65959): #This was a number I was getting repeatedly 24 | print "what." #When looking at riotgames (300k viewers). 25 | print namegroup #I still don't know why it is/was happening, so 26 | return 0 #it is still printing this for debugging purposes. 27 | return count - 1 28 | namegroup = namegroup.split(" ") 29 | if names_num in namegroup: 30 | names = namegroup[5:] 31 | count += len(names) 32 | if count == 2: 33 | print full_msg 34 | if count == 65959: #this was happening a lot, unexplicably. 35 | print "wath" 36 | return 0 37 | if count == 0: 38 | return count 39 | print "here" 40 | return count - 1 #don't count myself - i'm not actually in chat 41 | 42 | def chat_count(chatroom, verbose=False): 43 | global i 44 | i = 0 45 | chan = "#" + chatroom 46 | nick = get_username() 47 | PASS = get_password() 48 | sock = socket.socket() 49 | sock.connect(("irc.twitch.tv", default_port)) 50 | sock.send("PASS " + PASS + "\r\n") 51 | sock.send("USER " + nick + " 0 * :" + nick + "\r\n") 52 | sock.send("NICK " + nick + "\r\n") 53 | full_msg = "" 54 | while 1: 55 | sock.send("JOIN "+chan+"\r\n") 56 | data = remove_non_ascii(sock.recv(1024)) 57 | i+=1 58 | if data[0:4] == "PING": 59 | sock.send(data.replace("PING", "PONG")) 60 | continue 61 | full_msg += data #if you keep requesting to JOIN the channel, it will continue returning 62 | #more and more names until "End of /NAMES list" 63 | if verbose: 64 | print data 65 | if ":End of /NAMES list" in data: #do we end the search? 66 | if verbose: 67 | print "returning (\"End of /NAMES list\") due to:" 68 | print data 69 | return count_users(full_msg) 70 | if ":jtv MODE #" in data: 71 | if verbose: 72 | print "returning (FOUND MODE) due to:" 73 | print data 74 | return count_users(full_msg) 75 | if "366 " + nick in data: #status code for ending the stream of names 76 | if verbose: 77 | print "returning (366) due to:" 78 | print data 79 | return count_users(full_msg) 80 | if False and "PRIVMSG" in data: #privmsg's only come in after the names list 81 | if verbose: 82 | print "returning (PRIVMSG) due to:" 83 | print data 84 | return count_users(full_msg) 85 | 86 | def get_users(full_msg): 87 | l = [] 88 | data = full_msg.split("\r\n") 89 | for namegroup in data: 90 | if "End of /NAMES list" in namegroup or \ 91 | "tmi.twitch.tv " + end_names_num in namegroup: 92 | return l 93 | namegroup = namegroup.split(" ") 94 | if names_num in namegroup: 95 | names = namegroup[5:] 96 | for name in names: 97 | name = name.strip(":") 98 | if name != get_username(): 99 | l.append(name) 100 | print "here - i don't think this should occur." 101 | return l 102 | 103 | def user_follows(user): 104 | follows = requests.get("https://api.twitch.tv/kraken/users/"+user+"/follows/channels") 105 | return follows.json()['_total'] 106 | 107 | def avg_user_follows(user): 108 | r= requests.get("http://tmi.twitch.tv/group/user/" + user + "/chatters") 109 | chatters = r.json() 110 | l = chatters['chatters']['viewers'] 111 | skip = int(len(l) / 29.0) 112 | avg = 0 113 | cnt = 0 114 | print "counting..." 115 | for user in l[::skip]: 116 | cnt += 1 117 | total = user_follows(user) 118 | print cnt, user, total 119 | avg += total 120 | avg /= float(cnt) 121 | return avg 122 | 123 | #usage example: "python chat_count.py twitchplayspokemon" 124 | if __name__ == '__main__': 125 | if len(sys.argv) >= 2: 126 | args = sys.argv[1:] 127 | verbose = '-v' in args or '--verbose' in args 128 | print verbose 129 | count = 5 130 | #print avg_user_follows(sys.argv[1]) 131 | count = chat_count(sys.argv[1], verbose=verbose) 132 | print count, "chatters in %s" %sys.argv[1] 133 | 134 | -------------------------------------------------------------------------------- /get_exceptions.py.example: -------------------------------------------------------------------------------- 1 | #returns regular expressions 2 | #you don't have to put ^ or $ at the beginning/end. just use .* -- it's more readable. 3 | def get_exceptions(): 4 | return [r'twitch', r'esltv_.*'] #add more names here 5 | -------------------------------------------------------------------------------- /get_passwords.py.example: -------------------------------------------------------------------------------- 1 | def get_passwords(): 2 | APP_KEY = "TWITTERAPPKEY" 3 | APP_SECRET = "TWITTERAPPSECRET" 4 | 5 | OAUTH_TOKEN = "TWITTEROAUTHTOKEN" 6 | OAUTH_TOKEN_SECRET = "TWITTERTOKENSECRET" 7 | return [APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET] 8 | 9 | def get_twitter_name(): 10 | return "TwitterUserName" 11 | 12 | CLIENT_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 13 | 14 | -------------------------------------------------------------------------------- /global_consts.py: -------------------------------------------------------------------------------- 1 | debug = False #debug mode with extraneous error messages and information 2 | tweetmode = True #true if you want it to tweet, false if you don't 3 | d2l_check = False #check dota 2 lounge's website for embedded live matches? 4 | user_threshold = 200 #initial viewers necessity for confirmation 5 | ratio_threshold = 0.16 #if false positives, lower this number. if false negatives, raise this number 6 | expected_ratio = 0.7 #eventually tailor this to each game/channel. Tailoring to channel might be hard. 7 | num_games = 50 #number of games to look at, sorted by viewer count 8 | alternative_chatters_method = False #True if you want to use faster but potentially unreliable 9 | #method of getting number of chatters for a user 10 | 11 | 12 | -------------------------------------------------------------------------------- /handle_twitter.py: -------------------------------------------------------------------------------- 1 | from botter import Botter 2 | from global_consts import tweetmode, expected_ratio 3 | import sys 4 | import time 5 | 6 | if len(sys.argv) > 1: 7 | args = sys.argv[1:] 8 | if '--no-tweetmode' in args or '-q' in args: 9 | tweetmode = False 10 | 11 | if tweetmode: 12 | from twython import Twython 13 | from get_passwords import get_passwords, get_twitter_name 14 | import twitter 15 | 16 | if tweetmode: 17 | passes = get_passwords() 18 | 19 | # Must be a string - ex "BotDetectorBot" 20 | twitter_name = get_twitter_name() 21 | 22 | APP_KEY = passes[0] 23 | APP_SECRET = passes[1] 24 | OAUTH_TOKEN = passes[2] 25 | OAUTH_TOKEN_SECRET = passes[3] 26 | 27 | tweetter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) 28 | api = twitter.Api(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) 29 | 30 | num_recent_tweets = 50 31 | 32 | 33 | def get_formatted_game(game): 34 | """ 35 | tailors the name of the game (heh) to what is readable and short enough to tweet 36 | :param game: string 37 | """ 38 | formatted_game = game.split(":")[0] # manually shorten the tweet, many of these by inspection 39 | if formatted_game[:17] == "The Elder Scrolls": 40 | formatted_game = "TES:" + formatted_game[17:] # TES: Online 41 | if formatted_game == "Halo": 42 | formatted_game = game 43 | if formatted_game == "League of Legends": 44 | formatted_game = "LoL" 45 | if formatted_game == "Call of Duty" and len(game.split(":")) > 1: 46 | formatted_game = "CoD:" + game.split(":")[1] # CoD: Ghosts, CoD: Modern Warfare 47 | if formatted_game == "Counter-Strike" and len(game.split(":")) > 1: 48 | formatted_game = "CS: " 49 | for item in game.split(":")[1].split(" "): 50 | if len(item) > 0: 51 | formatted_game += item[0] # first initial - CS:S, CS:GO 52 | if formatted_game == "StarCraft II" and len(game.split(":")) > 1: 53 | formatted_game = "SC2: " 54 | for item in game.split(":")[1].split(" "): 55 | if len(item) > 0: 56 | formatted_game += item[0] # first initial - SC2: LotV 57 | return formatted_game 58 | 59 | 60 | # send_tweet 61 | def send_tweet(user, ratio, game, viewers, tweetmode, ratio_threshold, confirmed, suspicious): 62 | """ 63 | if is believed to be viewer botting, sends a tweet via the twitter module 64 | user is a string representing http://www.twitch.tv/ 65 | ratio is 's chatter to viewer ratio 66 | game is the game they're playing (Unabbreviated: ex. Starcraft II: Heart of the Swarm) 67 | viewers is how many viewers the person has - can be used to get number of chatters, with ratio 68 | """ 69 | name = "twitch.tv/" + user 70 | if ratio < ratio_threshold: 71 | found = False # Whether or not the user has been found in the *suspicious* list 72 | for item in confirmed: 73 | if item.user == name: 74 | item.ratio = ratio # update the info each time we go through it 75 | item.viewers = viewers 76 | item.chatters = int(viewers * ratio) 77 | item.game = game 78 | for item in suspicious: 79 | if item.user == name: 80 | item.viewers = viewers 81 | item.chatters = int(viewers * ratio) 82 | item.ratio = ratio # update the info 83 | item.game = game 84 | found = True 85 | if found: 86 | print 87 | if tweetmode: 88 | print "Tweeting!" 89 | else: 90 | print "(Not actually Tweeting this):" 91 | # move item from suspiciuos to confirmed 92 | confirmed.append([item for item in suspicious if item.user == name][0]) 93 | 94 | # usernames in these lists are unique (you can only stream once at a time...) 95 | suspicious = [item for item in suspicious if item.user != name] 96 | 97 | chatters = int(viewers * ratio) 98 | formatted_game = get_formatted_game(game) 99 | # TODO: change expected_ratio to be each game - is this a good idea? avg skewed by botting viewers, and low sample size... 100 | fake_viewers = int(viewers - (1 / expected_ratio) * chatters) 101 | name += "?live" 102 | estimate = "(~" + str(fake_viewers) + " extra viewers of " + str(viewers) + " total)" 103 | tweet = name + " (" + formatted_game + ") might have a false-viewer bot " + estimate 104 | if ratio < 0.07: 105 | tweet = name + " (" + formatted_game + ") appears to have a false-viewer bot " + estimate 106 | if ratio < 0.05: 107 | tweet = name + " (" + formatted_game + ") almost definitely has a false-viewer bot " + estimate 108 | if len(tweet) + 2 + len(user) <= 140: # Max characters in a tweet 109 | tweet = tweet + " #" + user 110 | if not tweetmode: 111 | print "Not", 112 | print "Tweet text: '" + tweet + "'" 113 | if tweetmode: 114 | while True: 115 | try: 116 | statuses = api.GetUserTimeline(twitter_name, count=num_recent_tweets)[:num_recent_tweets] 117 | break 118 | except twitter.TwitterError: 119 | print "error getting statuses for BDB - retrying" 120 | pass 121 | found_rec_tweet = False # Did we recently tweet about this person? 122 | for status in statuses: 123 | names = status.text.split("#") 124 | if len(names) == 2: 125 | if names[1] == user: 126 | found_rec_tweet = True 127 | break 128 | if found_rec_tweet: 129 | print "Not tweeting because I found recent tweet for", user 130 | else: 131 | try: 132 | tweetter.update_status(status=tweet) 133 | time.sleep(10) # Rate limiting 134 | except (KeyboardInterrupt, SystemExit): 135 | raise 136 | except: 137 | print "couldn't tweet :(" 138 | pass 139 | if not found: 140 | for item in confirmed: 141 | if item.user == name: 142 | print 143 | return 144 | new_botter = Botter(user=name, ratio=ratio, game=game, viewers=viewers, chatters=int(viewers * ratio)) 145 | suspicious.append(new_botter) 146 | print " <-- added to suspicious for this" 147 | else: 148 | print 149 | -------------------------------------------------------------------------------- /pass_info.py.example: -------------------------------------------------------------------------------- 1 | #info for twitch servers 2 | def get_username(): 3 | return "testuser" 4 | def get_password(): 5 | return "oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-twitter 3 | twython 4 | -------------------------------------------------------------------------------- /twitch_chatters.py: -------------------------------------------------------------------------------- 1 | import handle_twitter 2 | import re 3 | import socket 4 | import sys 5 | import time 6 | import urllib2 7 | 8 | import requests 9 | 10 | from get_exceptions import get_exceptions 11 | from get_passwords import CLIENT_ID 12 | from global_consts import debug, tweetmode, alternative_chatters_method, \ 13 | d2l_check, user_threshold, ratio_threshold, \ 14 | num_games 15 | from twitch_viewers import user_viewers, remove_non_ascii 16 | from utils import get_json_response 17 | 18 | if len(sys.argv) > 1: 19 | args = sys.argv[1:] 20 | if '-debug' in args: 21 | debug = True 22 | if '--no-tweetmode' in args or '-q' in args: 23 | tweetmode = False 24 | 25 | 26 | if debug: 27 | import webbrowser # Just for debugging. 28 | if alternative_chatters_method: # From what I can tell, this no longer works. I believe it has something to do with the backend of how their IRC is implemented. 29 | from chat_count import chat_count 30 | 31 | global_sum = 0 32 | global_cnt = 0 33 | 34 | # Lists of Botters passed around all over the place, represents who's currently botting. 35 | suspicious = [] 36 | confirmed = [] 37 | 38 | # These users are known to have small chat to viewer ratios for valid reasons 39 | # NOTE: Regexes, not only strings (though strings will work too) 40 | # You don't have to put ^ or $ at the beginning/end. just use .* -- it's more readable. 41 | # example: chat disabled, or chat hosted not on the twitch site, or mainly viewed on 42 | # front page of twitch 43 | # type: list of REGEXes: example: ["destiny", "scg_live.*", ".*twitch.*"] 44 | exceptions = get_exceptions() 45 | 46 | 47 | def get_chatters2(user): 48 | """ 49 | gets the number of chatters in user's Twitch chat, via chat_count 50 | Essentially, chat_count is my experimental method that goes directly to 51 | a user's IRC channel and counts the viewers there. It is not yet proven to be 52 | correct 100% of the time. 53 | """ 54 | chatters2 = 0 55 | try: 56 | chatters2 = chat_count(user) 57 | except socket.error as error: 58 | print ":((( get_chatters2 line 37 on twitch_chatters" 59 | return get_chatters2(user) 60 | return chatters2 61 | 62 | 63 | def user_chatters(user, depth=0): 64 | """ 65 | Returns the number of chatters in user's Twitch chat 66 | :param user: string representing http://www.twitch.tv/ 67 | """ 68 | chatters = 0 69 | chatters2 = 0 70 | try: 71 | req = requests.get("http://tmi.twitch.tv/group/user/" + user, headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 72 | except (KeyboardInterrupt, SystemExit): 73 | raise 74 | except: 75 | print "couldn't get users for " + user + "; recursing" 76 | time.sleep(0.3) # don't recurse too fast 77 | return user_chatters(user, depth + 1) 78 | if alternative_chatters_method: 79 | chatters2 = get_chatters2(user) 80 | if chatters2 > 1: 81 | return chatters2 82 | try: 83 | while req.status_code != 200: 84 | print "----TMI error", req.status_code, 85 | if alternative_chatters_method: 86 | chatters2 = get_chatters2(user) 87 | print "getting", user + " (module returned %d)-----" % chatters2 88 | if chatters2 > 1: 89 | return chatters2 90 | else: 91 | print "getting", user + "-----" 92 | return user_chatters(user, depth + 1) 93 | try: 94 | chat_data = req.json() 95 | except ValueError: 96 | print "couldn't json in getting " + user + "'s chatters; recursing" 97 | return user_chatters(user, depth + 1) 98 | chatters = chat_data['chatter_count'] 99 | except (KeyboardInterrupt, SystemExit): 100 | raise 101 | except: 102 | print "recursing in user_chatters, got some kinda TypeError" 103 | return user_chatters(user, depth + 1) 104 | return chatters 105 | 106 | 107 | # dota2lounge_list: 108 | def get_dota2lounge_list(): 109 | """ 110 | returns the list of live Twitch streams embedded on dota2lounge. 111 | this is useful because, at any given time, there could be tens of thousands 112 | of users watching a Twitch stream through d2l, and I don't want to false positive these streams. 113 | """ 114 | try: 115 | req = urllib2.Request('http://dota2lounge.com/index.php') 116 | req.add_header('Client-ID', CLIENT_ID) 117 | req.add_header('Accept', 'application/vnd.twitchtv.v5+json') 118 | u = urllib2.urlopen(req).read().split("matchmain") 119 | except (KeyboardInterrupt, SystemExit): 120 | raise 121 | except: 122 | print "D2L error 1 :(((" 123 | return [] 124 | string = "LIVE" 125 | list1 = filter(lambda x: string in x, u) 126 | 127 | list2 = [] 128 | string2 = "match?m=" 129 | for item in list1: 130 | item = item.split("\n") 131 | for sentence in item: 132 | if string2 in sentence: 133 | list2.append(sentence) 134 | 135 | d2l_list = [] 136 | 137 | for item in list2: 138 | url = "http://dota2lounge.com/" + item.split("\"")[1] 139 | try: 140 | req = urllib2.Request(url) 141 | req.add_header('Client-ID', CLIENT_ID) 142 | req.add_header('Accept', 'application/vnd.twitchtv.v5+json') 143 | u2 = urllib2.urlopen(req).read().split("\n") 144 | except (KeyboardInterrupt, SystemExit): 145 | raise 146 | except: 147 | print "D2L error 2 :(((" 148 | return [] 149 | list3 = filter(lambda x: "twitch.tv/widgets/live_embed_player.swf?channel=" in x, u2) 150 | for item in list3: 151 | item = item.split("channel=")[1].split("\"")[0].lower() 152 | d2l_list.append(item) 153 | return d2l_list 154 | 155 | 156 | def get_frontpage_users(): 157 | """ 158 | Returns a list of featured streamers. 159 | """ 160 | try: 161 | url = "https://api.twitch.tv/kraken/streams/featured?limit=100" 162 | req = requests.get(url, headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 163 | data = req.json() 164 | except (KeyboardInterrupt, SystemExit): 165 | raise 166 | except Exception as e: 167 | print("Error getting featured streams: ", e) 168 | return [] 169 | return [obj['stream']['channel']['name'] for obj in data['featured']] 170 | 171 | 172 | def is_being_hosted(user): 173 | user_dict = get_json_response('https://api.twitch.tv/kraken/channels/{}'.format(user)) 174 | if '_id' not in user_dict: 175 | return False 176 | user_id = user_dict['_id'] 177 | hosts = get_json_response('https://tmi.twitch.tv/hosts?include_logins=1&target={}'.format(user_id)) 178 | if 'hosts' not in hosts: 179 | return False 180 | return hosts['hosts'] != [] 181 | 182 | 183 | def user_ratio(user): 184 | """ 185 | :param user: string representing http://www.twitch.tv/ 186 | :return: the ratio of chatters to viewers in 's channel 187 | """ 188 | chatters2 = 0 189 | exceptions = get_exceptions() 190 | # Don't have to put ^ or $ at the beginning. Just use .* it's more concise. 191 | for regex in exceptions: 192 | if regex != '': 193 | if regex[0] != '^': 194 | regex = '^' + regex 195 | if regex[-1] != '$': 196 | regex += '$' 197 | if re.match(regex, user, re.I | re.S) != None: 198 | print user, "is alright :)", 199 | return 1 200 | if user in get_frontpage_users(): 201 | print "nope,", user, "is a featured stream (being shown on the frontpage).", 202 | return 1 203 | if is_being_hosted(user): 204 | print "nope,", user, "is being hosted by someone", 205 | return 1 206 | if d2l_check: 207 | d2l_list = get_dota2lounge_list() 208 | if user in d2l_list: 209 | print user, "is being embedded in dota2lounge. nogo", 210 | return 1 211 | chatters = user_chatters(user) 212 | if debug: 213 | chatters2 = get_chatters2(user) 214 | viewers = user_viewers(user) 215 | if viewers == -1: # This means something went wrong with the twitch servers, or internet cut out 216 | print "RECURSING BECAUSE OF 422 TWITCH ERROR" 217 | return user_ratio(user) 218 | if viewers and viewers != 0: # viewers == 0 => streamer offline 219 | maxchat = max(chatters, chatters2) 220 | ratio = float(maxchat) / viewers 221 | print user + ": " + str(maxchat) + " / " + str(viewers) + " = %0.3f" % ratio, 222 | if debug: 223 | print "(%d - %d)" % (chatters2, chatters), 224 | if chatters != 0: 225 | if debug: 226 | diff = abs(chatters2 - chatters) 227 | error = (100 * (float(diff) / chatters)) # Percent error 228 | else: 229 | return 0 230 | if debug and error > 6: 231 | print " (%0.0f%% error)!" % error, 232 | if error < 99 and diff > 10: 233 | print "!!!!!!!!!!!!!!!!!!!" # If my chatters module goes wrong, i want to notice it. 234 | if ratio > 1: 235 | webbrowser.open("BDB - ratio for " + user + " = %0.3f" % ratio) 236 | print "????????????" 237 | else: 238 | print 239 | else: 240 | return 1 # User is offline. 241 | return ratio 242 | 243 | 244 | def game_ratio(game): 245 | """ 246 | Returns the average chatter:viewer ratio for a certain game 247 | :param game: string (game to search) 248 | :return: 249 | """ 250 | global tweetmode 251 | try: 252 | r = requests.get('https://api.twitch.tv/kraken/streams?game=' + game, headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 253 | except (KeyboardInterrupt, SystemExit): 254 | raise 255 | except: 256 | print "uh oh caught exception when connecting. try again. see game_ratio(game)." 257 | time.sleep(5) 258 | return game_ratio(game) 259 | if not r: 260 | time.sleep(5) 261 | return game_ratio(game) 262 | while r.status_code != 200: 263 | print r.status_code, ", service unavailable" 264 | r = requests.get('https://api.twitch.tv/kraken/streams?game=' + game, headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 265 | try: 266 | gamedata = r.json() 267 | except ValueError: 268 | print "could not decode json. recursing" 269 | time.sleep(5) 270 | return game_ratio(game) 271 | # TODO make a dictionary with keys as the game titles and values as the average and count 272 | count = 0 # Number of games checked 273 | avg = 0 274 | while 'streams' not in gamedata.keys(): 275 | r = requests.get('https://api.twitch.tv/kraken/streams?game=' + game, headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 276 | while r.status_code != 200: 277 | print r.status_code, ", service unavailable" 278 | r = requests.get('https://api.twitch.tv/kraken/streams?game=' + game, headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 279 | time.sleep(1) 280 | try: 281 | gamedata = r.json() 282 | except ValueError: 283 | print "couldn't json; recursing" 284 | continue 285 | if len(gamedata['streams']) > 0: 286 | for i in range(0, len(gamedata['streams'])): 287 | viewers = gamedata['streams'][i]['viewers'] 288 | if viewers < user_threshold: 289 | break 290 | 291 | user = gamedata['streams'][i]['channel']['name'].lower() 292 | 293 | ratio = user_ratio(user) 294 | if ratio == 0: 295 | print "ratio is 0... abort program?" 296 | handle_twitter.send_tweet(user, ratio, game, viewers, tweetmode, 297 | ratio_threshold, confirmed, suspicious) 298 | avg += ratio 299 | count += 1 300 | time.sleep(1) # don't spam servers 301 | else: 302 | print "couldn't find " + game + " :(" 303 | return 0 304 | global global_sum 305 | global global_cnt 306 | global_sum += avg 307 | global_cnt += count 308 | if count != 0: 309 | avg /= count 310 | # For the game specified, go through all users more than viewers, find ratio, average them. 311 | return avg 312 | 313 | 314 | def remove_offline(): 315 | """ 316 | Removes users from the suspicious and confirmed lists if they are no longer botting 317 | """ 318 | print "==REMOVING OFFLINE==" 319 | flag = False # flag is for styling the terminal, nothing else. 320 | to_remove = [] 321 | for item in suspicious: 322 | name = item.user 323 | originame = name[10:] # Remove the http://www.twitch.tv/ 324 | if (user_ratio(originame) > 2 * ratio_threshold or 325 | user_viewers(originame) < user_threshold / 4): 326 | print originame + " appears to have stopped botting! removing from suspicious list" 327 | to_remove.append(item) 328 | else: 329 | print 330 | for item in to_remove: 331 | suspicious.remove(item) 332 | to_remove = [] 333 | for item in confirmed: 334 | if confirmed != []: 335 | flag = True # Flag is for styling the terminal, nothing else. 336 | name = item.user 337 | originame = name[10:] # remove the http://www.twitch.tv/ 338 | if user_ratio(originame) > (2 * ratio_threshold) or user_viewers(originame) < 50: 339 | print originame + " appears to have stopped botting! removing from confirmed list" 340 | to_remove.append(item) 341 | else: 342 | print 343 | for item in to_remove: 344 | confirmed.remove(item) 345 | if flag: 346 | print 347 | print 348 | print "looping back around :D" 349 | print 350 | print 351 | 352 | 353 | def search_all_games(): 354 | """ 355 | loops through all the games via the Twitch API, checking for their average ratios 356 | """ 357 | global global_sum 358 | try: 359 | topreq = requests.get("https://api.twitch.tv/kraken/games/top?limit=" + str(num_games), 360 | headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 361 | while topreq.status_code != 200: 362 | print "trying to get top games..." 363 | topreq = requests.get("https://api.twitch.tv/kraken/games/top?limit=" + str(num_games), 364 | headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 365 | topdata = topreq.json() 366 | except requests.exceptions.ConnectionError: 367 | print "connection error trying to get the game list. recursing :)))" 368 | return search_all_games 369 | except ValueError: 370 | print "nope. recursing. ~287 twitch_chatters.py" 371 | search_all_games() 372 | for i in range(0, len(topdata['top'])): 373 | game = remove_non_ascii(topdata['top'][i]['game']['name']) 374 | print "__" + game + "__", 375 | print "(tweetmode off)" if not tweetmode else "" 376 | prev_suspicious = suspicious[:] # Make a duplicate of suspicious before things are added to the new suspicious list 377 | ratio = game_ratio(game) # Remove elements from suspicious and puts them into confirmed 378 | for item in suspicious: 379 | if item.game == game and item in prev_suspicious: 380 | newconfirmed = [i for i in confirmed if i.game == game and item.user == i.user] 381 | if newconfirmed != []: 382 | suspicious.remove(item) 383 | print item.user[10:], "was found to have stopped botting", game + "!", 384 | print " removing from suspicious list!" 385 | else: 386 | suspicious.remove(item) 387 | print 388 | print "Average ratio for " + game + ": %0.3f" % ratio 389 | print 390 | print "Total global ratio: %0.3f" % (global_sum / float(global_cnt)) 391 | print 392 | print "We are suspicious of: " 393 | if len(suspicious) == 0: 394 | print "No one :D" 395 | for item in suspicious: 396 | channel = item.user[10:] 397 | print "%s %s%d / %d = %0.3f %s" % (channel, 398 | " " * (20 - len(channel)), # formatting spaces 399 | item.chatters, item.viewers, item.ratio, 400 | item.game 401 | ) 402 | print 403 | print "We have confirmed: " 404 | if len(confirmed) == 0: 405 | print "No one :D" 406 | for item in confirmed: 407 | channel = item.user[10:] 408 | print "%s %s%d / %d = %0.3f %s" % (channel, 409 | " " * (20 - len(channel)), # formatting spaces 410 | item.chatters, item.viewers, item.ratio, 411 | item.game 412 | ) 413 | print 414 | print "Total of", len(suspicious) + len(confirmed), "botters" 415 | print 416 | print 417 | -------------------------------------------------------------------------------- /twitch_viewers.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import sys # for printing to stderr and restarting program 3 | import os 4 | import time 5 | 6 | from get_passwords import CLIENT_ID 7 | 8 | restart_on_failure = False 9 | 10 | 11 | def remove_non_ascii(s): return "".join([x if ord(x) < 128 else '?' for x in s]) 12 | 13 | 14 | # thank you, stack overflow 15 | def restart_program(): 16 | python = sys.executable 17 | os.execl(python, python, *sys.argv) 18 | 19 | 20 | def user_total_views(user): 21 | """ 22 | Returns the number of total views twitch.tv/user has had. 23 | :param user: string representing http://www.twitch.tv/ 24 | """ 25 | try: 26 | r = requests.get("https://api.twitch.tv/kraken/search/channels?q=" + user, 27 | headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 28 | except (KeyboardInterrupt, SystemExit): 29 | raise 30 | except: 31 | print "error getting the total views for", user + "; recursing." 32 | time.sleep(1) 33 | return user_total_views(user) 34 | while r.status_code != 200: 35 | try: 36 | r = requests.get("https://api.twitch.tv/kraken/search/channels?q=" + user, 37 | headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 38 | break 39 | except (KeyboardInterrupt, SystemExit): 40 | raise 41 | except: 42 | print "error getting the total views for", user + "; recursing." 43 | time.sleep(1) 44 | return user_total_views(user) 45 | chan = r.json() 46 | if chan['channels'][0]['name'] == user: 47 | return chan['channels'][0]['views'] 48 | 49 | 50 | def user_viewers(user): 51 | """ 52 | returns the number of viewers twitch.tv/user currently has. returns 0 if offline. 53 | 54 | :param user: string representing http://www.twitch.tv/ 55 | """ 56 | global restart_on_failure 57 | req = 0 58 | try: 59 | req = requests.get("https://api.twitch.tv/kraken/streams/" + user, 60 | headers={"Client-ID": CLIENT_ID, "Accept": "application/vnd.twitchtv.v5+json"}) 61 | except (KeyboardInterrupt, SystemExit): 62 | raise 63 | except Exception, e: 64 | print e 65 | print "error getting the current views for", user + "; recursing." 66 | time.sleep(1) 67 | return user_viewers(user) 68 | i = 0 69 | while req.status_code != 200: 70 | print req.status_code, "viewerlist unavailable (due to %s)" % user 71 | try: 72 | import urllib2 73 | import json 74 | req2 = urllib2.Request("https://api.twitch.tv/kraken/streams/" + user) 75 | req2.add_header('Client-ID', CLIENT_ID) 76 | req2.add_header('Accept', 'application/vnd.twitchtv.v5+json') 77 | response = urllib2.urlopen(req2) 78 | try: 79 | userdata = json.load(response) 80 | except ValueError: 81 | print "couldn't json. recursing (line 65 twitch_viewers)" 82 | time.sleep(0.5) 83 | return user_viewers(user) # nope start over 84 | if 'stream' in userdata.keys(): 85 | viewers = 0 86 | if userdata['stream']: # if the streamer is offline, userdata returns null 87 | viewers = userdata['stream']['viewers'] 88 | if viewers == 0: 89 | print user + " appears to be offline!", 90 | return viewers 91 | else: 92 | print user 93 | print str(userdata['status']) + " " + userdata['message'] + " " + userdata['error'] 94 | print user + " is not live right now, or the API is down." 95 | return 0 96 | except (KeyboardInterrupt, SystemExit): 97 | raise 98 | except Exception, e: 99 | print e 100 | print "error getting viewers for " + user 101 | time.sleep(1) 102 | pass 103 | if i > 15: 104 | if restart_on_failure: 105 | print "RESTARTING PROGRAM!!!!!!!!!!!!!!!!!!!!! 422 ERROR" 106 | restart_program() 107 | else: 108 | print "quitting fn due to", user 109 | return 0 110 | if req.status_code == 422 or req.status_code == 404: 111 | i += 1 112 | try: 113 | userdata = req.json() 114 | except ValueError: 115 | print "couldn't json. recursing (line 113 twitch_viewers)" 116 | time.sleep(1) 117 | return user_viewers(user) # Nope start over 118 | if 'stream' in userdata.keys(): 119 | viewers = 0 120 | if userdata['stream']: # If the streamer is offline, userdata returns null 121 | viewers = userdata['stream']['viewers'] 122 | if viewers == 0: 123 | print user + " appears to be offline!", 124 | return viewers 125 | else: 126 | print user 127 | print str(userdata['status']) + " " + userdata['message'] + " " + userdata['error'] 128 | print user + " is not live right now, or the API is down." 129 | return 0 130 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from get_passwords import CLIENT_ID 3 | 4 | def get_json_response(url): 5 | try: 6 | req = requests.get(url, headers={"Client-ID": CLIENT_ID}) 7 | data = req.json() 8 | return data 9 | except (KeyboardInterrupt, SystemExit): 10 | raise 11 | except Exception as e: 12 | print("Error getting " + url + ":", e) 13 | return [] 14 | 15 | --------------------------------------------------------------------------------