├── auth.py ├── README.md ├── .gitignore └── bot.py /auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from google_auth_oauthlib.flow import InstalledAppFlow 4 | 5 | load_dotenv() 6 | client_id = os.getenv('CLIENT_ID') 7 | client_secret = os.getenv('CLIENT_SECRET') 8 | 9 | 10 | def Authorize(file): 11 | flow = InstalledAppFlow.from_client_secrets_file(file, scopes={ 12 | 'openid', 13 | 'https://www.googleapis.com/auth/userinfo.email', 14 | 'https://www.googleapis.com/auth/userinfo.profile', 15 | 'https://www.googleapis.com/auth/youtube', 16 | 'https://www.googleapis.com/auth/youtube.force-ssl', 17 | 'https://www.googleapis.com/auth/youtube.readonly', 18 | }) 19 | flow.run_local_server( 20 | host='localhost', 21 | port=5500, 22 | authorization_prompt_message="") 23 | return flow 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube Live Stream Chatbot 2 | 3 | ### This Chat Bot was developed using YouTube Data & Live Streaming API v3 in Python 4 | 5 | The bot replies to live chat messages in a Live Stream (obviously :p). It currently has a limited set of commands to respond to, but you can easily add more. 6 | 7 | ## ⬇️ How to setup 8 | 9 | 1. Goto [Google Developers Console](https://console.developers.google.com/) and create a new project. 10 | 11 | 2. Enable YouTube Data API v3 12 | 3. Then from the Credentials tab, create a new OAuth Client ID and select the Web Application option. In the "Authorized redirect URIs" section, add the redirect URI you want to use. In the case of this project you will type "http://localhost:5500/" (you can use any port but I am specifically using 5500 as I have declared that as my auth uri in the auth.py script) and click Save. 13 | 4. Create a `.env` file in your project directory and copy/paste the Client ID and Client Secret in the file. 14 | 5. Then Copy & Paste the following commands in your terminal: 15 | 16 | ``` 17 | git clone https://github.com/gulraiznoorbari/YouTube_Live_Stream_Chat_Bot.git 18 | cd YouTube_Live_Stream_Chat_Bot 19 | python bot.py 20 | ``` 21 | 22 | The bot will ask you to authenticate yourself with your Google Account. 23 | 24 | After that, you will be prompted to enter a Live Stream Id in you terminal: 25 | 26 | > www.youtube.com/watch?v=qDWw4sAD3zY 27 | 28 | Here, the Live Stream Id is **`qDWw4sAD3zY`** 29 | 30 | ## 👨‍💻 Author 31 | 32 | You can get in touch with me on my LinkedIn Profile: 33 | 34 | #### Gulraiz Noor Bari 35 | 36 | [![LinkedIn Link](https://img.shields.io/badge/Connect-gulraiznoorbari-blue.svg?logo=linkedin&longCache=true&style=social&label=Connect)](https://www.linkedin.com/in/gulraiznoorbari) 37 |
38 | You can also follow my GitHub Profile to stay updated about my latest projects: [![GitHub Follow](https://img.shields.io/badge/Connect-gulraiznoorbari-blue.svg?logo=Github&longCache=true&style=social&label=Follow)](https://github.com/gulraiznoorbari) 39 | 40 | If you liked the repo then kindly support it by giving it a star ⭐! 41 | 42 | ## Contributions Welcome 43 | 44 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](#) 45 | 46 | If you find any bug in the code or have any improvements in mind then feel free to generate a pull request. 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client_secret.json 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import random 4 | from googleapiclient.discovery import build 5 | from auth import Authorize 6 | 7 | authResponse = Authorize('client_secret.json') 8 | credentials = authResponse.credentials 9 | 10 | # Building the youtube object: 11 | youtube = build('youtube', 'v3', credentials=credentials) 12 | 13 | # Settings 14 | _delay = 1 15 | 16 | 17 | 18 | def getLiveChatId(LIVE_STREAM_ID): 19 | """ 20 | It takes a live stream ID as input, and returns the live chat ID associated with that live stream 21 | 22 | LIVE_STREAM_ID: The ID of the live stream 23 | return: The live chat ID of the live stream. 24 | """ 25 | 26 | stream = youtube.videos().list( 27 | part="liveStreamingDetails", 28 | id=LIVE_STREAM_ID, # Live stream ID 29 | ) 30 | response = stream.execute() 31 | # print("\nLive Stream Details: ", json.dumps(response, indent=2)) 32 | 33 | liveChatId = response['items'][0]['liveStreamingDetails']['activeLiveChatId'] 34 | print("\nLive Chat ID: ", liveChatId) 35 | return liveChatId 36 | 37 | 38 | # Access user's channel Name: 39 | def getUserName(userId): 40 | """ 41 | It takes a userId and returns the userName. 42 | 43 | userId: The user's YouTube channel ID 44 | return: User's Channel Name 45 | """ 46 | channelDetails = youtube.channels().list( 47 | part="snippet", 48 | id=userId, 49 | ) 50 | response = channelDetails.execute() 51 | # print(json.dumps(response, indent=2)) 52 | userName = response['items'][0]['snippet']['title'] 53 | return userName 54 | # print(getUserName("UC0YXSy_J8uTDEr7YX_-d-sg")) 55 | 56 | 57 | def sendReplyToLiveChat(liveChatId, message): 58 | """ 59 | It takes a liveChatId and a message, and sends the message to the live chat. 60 | 61 | liveChatId: The ID of the live chat to which the message should be sent 62 | message: The message you want to send to the chat 63 | """ 64 | reply = youtube.liveChatMessages().insert( 65 | part="snippet", 66 | body={ 67 | "snippet": { 68 | "liveChatId": liveChatId, 69 | "type": "textMessageEvent", 70 | "textMessageDetails": { 71 | "messageText": message, 72 | } 73 | } 74 | } 75 | ) 76 | response = reply.execute() 77 | print("Message sent!") 78 | 79 | 80 | def main(): 81 | LIVE_STREAM_ID = input("Enter the live stream ID: ") 82 | # LIVE_STREAM_ID = "zxJ01IK_9z0" 83 | liveChatId = getLiveChatId(LIVE_STREAM_ID) 84 | messagesList = [] # List of messages 85 | 86 | while True: 87 | # bot replies to every message within past 1 second (can be changed to add delay): 88 | time.sleep(1) 89 | 90 | notReadMessages = [] # List of messages not yet read by bot 91 | 92 | # Fetching the messages from the live chat: 93 | liveChat = youtube.liveChatMessages().list( 94 | liveChatId=liveChatId, 95 | part="snippet" 96 | ) 97 | response = liveChat.execute() 98 | # print("\nMessages Fetched: ", json.dumps(response, indent=2)) 99 | allMessages = response['items'] 100 | 101 | # Check if there are any new messages and add them messagesList/notReadMessages list: 102 | if len(messagesList) >= 0: 103 | for messages in allMessages: 104 | userId = messages['snippet']['authorChannelId'] 105 | message = messages['snippet']['textMessageDetails']['messageText'] 106 | messagesList.append((userId, message)) 107 | else: 108 | for messages in allMessages: 109 | userId = messages['snippet']['authorChannelId'] 110 | message = messages['snippet']['textMessageDetails']['messageText'] 111 | if (userId, message) not in messagesList: 112 | notReadMessages.append((userId, message)) 113 | if (userId, message) not in messagesList: 114 | messagesList.append((userId, message)) 115 | print("New Message: ", notReadMessages) 116 | 117 | for message in notReadMessages: 118 | userId = message[0] 119 | message = message[1] 120 | userName = getUserName(userId) 121 | print(f'\nUsername: {userName}') 122 | 123 | if (message == "Hello" or message == "hello" or message == "Hi" or message == "hi"): 124 | sendReplyToLiveChat( 125 | liveChatId, 126 | "Hey " + userName + "! Welcome to the stream!") 127 | 128 | if (message == "!discord" or message == "!disc"): 129 | discord_link = "https://discord.gg/" 130 | sendReplyToLiveChat( 131 | liveChatId, 132 | f'Join our discord! {discord_link}') 133 | 134 | if (message == "!random" or message == "!rand"): 135 | dad_jokes = [ 136 | "Why do fathers take an extra pair of socks when they go golfing? In case they get a hole in one!", 137 | "Dear Math, grow up and solve your own problems.", 138 | "What has more letters than the alphabet? The post office!", 139 | "Why are elevator jokes so classic and good? They work on so many levels!", 140 | "What do you call a fake noodle? An impasta!", 141 | "What do you call a belt made out of watches? A waist of time!", 142 | "Why did the scarecrow win an award? Because he was outstanding in his field!", 143 | "Why don't skeletons ever go trick or treating? Because they have no body to go with!", 144 | "What's brown and sticky? A stick!"] 145 | joke = random.choice(dad_jokes) 146 | sendReplyToLiveChat(liveChatId, joke) 147 | 148 | 149 | if __name__ == "__main__": 150 | main() 151 | --------------------------------------------------------------------------------