├── .gitignore ├── LICENSE ├── README.md ├── bot.ini.example ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | 4 | bin/ 5 | include/ 6 | lib/ 7 | lib64 8 | pip-selfcheck.json 9 | pyvenv.cfg 10 | share/ 11 | 12 | bot.ini 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 James Curbo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twitch slack notification bot 2 | 3 | This is a simple bot that notifies your Slack and Discord channel(s) when people start 4 | streaming on Twitch. 5 | 6 | # Requirements & Configuration 7 | 8 | It's written in Python 3 and the only external dependency is the 'requests' 9 | library. 10 | 11 | Copy 'bot.ini.example' to 'bot.ini' and modify as necessary. You'll need to 12 | setup Slack and Discord incoming webhooks, as well as obtain a Twitch client-ID. (Twitch 13 | no longer allows API access without a client ID.) 14 | 15 | Note for Discord: make sure you use the Slack-compatible webhook URL. 16 | 17 | # Usage 18 | 19 | Run main.py, preferably within a screen or tmux instance. It will poll your 20 | chosen list of users every 60 seconds looking for changes. To exit simply hit 21 | ctrl-C. 22 | 23 | # To Do 24 | 25 | Currently I'm using this in both Slack and Discord so it assumes both webhooks exist. I need to add checking to see if one or the other webhook config items are blank and send messages accordingly. 26 | 27 | # Author 28 | 29 | James Curbo 30 | 31 | 32 | -------------------------------------------------------------------------------- /bot.ini.example: -------------------------------------------------------------------------------- 1 | [client] 2 | # Twitch API base URL 3 | baseurl = https://api.twitch.tv/kraken/streams/ 4 | # Twitch client id you need to obtain 5 | # see https://blog.twitch.tv/client-id-required-for-kraken-api-calls-afbb8e95f843#.defkd46le 6 | client-id = 7 | # Comma separated list of usernames to follow 8 | usernames = magfest, LAGTVMaximusBlack 9 | # Slack webhook to use - you'll need to create an Incoming Webhook for your 10 | # Slack instance 11 | slack_webhook = 12 | # Discord webhook to use - make sure you append /slack - see Discord webhook 13 | # docs for more info 14 | discord_webhook = 15 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import configparser 3 | import time 4 | 5 | clientinfo = {} 6 | streams_current = {} 7 | 8 | def main(): 9 | configfile = 'bot.ini' 10 | config = configparser.ConfigParser() 11 | config.read(configfile) 12 | 13 | # TODO: if we want to do more than access stream objects, baseurl should 14 | # be pointed at just api.twitch.tv and another variable added to control 15 | # which part of the API to go to (streams, channels, users, etc) 16 | clientinfo['baseurl'] = config['client']['baseurl'] 17 | clientinfo['client-id'] = config['client']['client-id'] 18 | clientinfo['usernames'] = [e.strip() for e in config['client']['usernames'].split(',')] 19 | clientinfo['slack_webhook'] = config['client']['slack_webhook'] 20 | clientinfo['discord_webhook'] = config['client']['discord_webhook'] 21 | 22 | starttime = time.time() 23 | 24 | while True: 25 | mainloop() 26 | time.sleep(60.0 - ((time.time() - starttime) % 60.0)) 27 | 28 | def mainloop(): 29 | print("running main loop") 30 | global streams_current 31 | 32 | print("Current list of stream ids: {}".format(list(streams_current.keys()))) 33 | 34 | # Get the current status of all users 35 | streams_new = get_streams() 36 | # Compare against the previous iteration of the loop 37 | # If this is the first time, streams_current will be empty so everything will be 38 | # new 39 | streamids_status = compare_streams(streams_current, streams_new) 40 | # Announce changes 41 | announce_streams(streamids_status, streams_current, streams_new) 42 | # copy new streams list to main list for next loop 43 | streams_current = streams_new 44 | 45 | def get_streams(): 46 | streams = {} 47 | headers = {'Client-ID': clientinfo['client-id']} 48 | 49 | for user in clientinfo['usernames']: 50 | print("Checking {}".format(user)) 51 | # note: the API url is something like: api.twitch.tv/kraken/streams/hannibal127 52 | r = requests.get(clientinfo['baseurl'] + user, headers=headers) 53 | res = r.json() 54 | 55 | print(res) 56 | 57 | if res['stream'] != None: 58 | stream_id = res['stream']['_id'] 59 | streams[stream_id] = {} 60 | streams[stream_id]['username'] = user 61 | streams[stream_id]['game'] = res['stream']['game'] 62 | streams[stream_id]['url'] = res['stream']['channel']['url'] 63 | streams[stream_id]['status'] = res['stream']['channel']['status'] 64 | streams[stream_id]['preview_l'] = res['stream']['preview']['large'] 65 | 66 | return streams 67 | 68 | def compare_streams(streams_current, streams_new): 69 | streamids_changed = {} 70 | streamids_changed['offline'] = [] 71 | streamids_changed['online'] = [] 72 | 73 | print("Current streams: {}".format(list(streams_current.keys()))) 74 | print("New streams: {}".format(list(streams_new.keys()))) 75 | 76 | for key in streams_current: 77 | if key not in streams_new: 78 | streamids_changed['offline'].append(key) 79 | 80 | for key in streams_new: 81 | if key not in streams_current: 82 | streamids_changed['online'].append(key) 83 | 84 | print("Streams that just went offline: {}".format(streamids_changed['offline'])) 85 | print("Streams that just went online: {}".format(streamids_changed['online'])) 86 | 87 | return streamids_changed 88 | 89 | def announce_streams(streamids_changed, streams_current, streams_new): 90 | for streamid in streamids_changed['online']: 91 | ann_text = "{} has gone live on Twitch!".format(streams_new[streamid]['username']) 92 | att_text = "{}\nStatus: {}\nGame: {}".format( 93 | streams_new[streamid]['url'], 94 | streams_new[streamid]['status'], 95 | streams_new[streamid]['game']) 96 | 97 | payload = { 98 | 'text': ann_text, 99 | 'attachments': [ 100 | { 101 | 'color': '#00FF00', 102 | 'text': att_text, 103 | 'image_url': streams_new[streamid]['preview_l'] 104 | } 105 | ] 106 | } 107 | 108 | p = requests.post(clientinfo['slack_webhook'], json=payload) 109 | q = requests.post(clientinfo['discord_webhook'], json=payload) 110 | 111 | for streamid in streamids_changed['offline']: 112 | ann_text = "{} stopped streaming on Twitch.".format(streams_current[streamid]['username']) 113 | att_text = "{}\nStatus: {}\nGame: {}".format( 114 | streams_current[streamid]['url'], 115 | streams_current[streamid]['status'], 116 | streams_current[streamid]['game']) 117 | 118 | payload = { 119 | 'text': ann_text, 120 | 'attachments': [ 121 | { 122 | 'color': '#FF0000', 123 | 'text': att_text 124 | } 125 | ] 126 | } 127 | p = requests.post(clientinfo['slack_webhook'], json=payload) 128 | q = requests.post(clientinfo['discord_webhook'], json=payload) 129 | 130 | if __name__ == "__main__": 131 | main() 132 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | configparser==3.5.0 2 | requests==2.12.5 3 | --------------------------------------------------------------------------------