popcorn9499 / Youtube-Discord-Relay-Bot

This is a youtube and discord chat relay bot
MIT License
9 stars 4 forks source link

Bot not sending YT chat messages to discord. #12

Closed jayKeegan closed 3 years ago

jayKeegan commented 3 years ago

Hey. I have set up the bot, and it comes online, etc, it isn't sending messages to the discord channel, any ideas why? It has full admin permissions. Thank you!

I have attached the python code below, since I have changed some stuff due to google API changes, and provided a stream ID.


import json
import re
#youtube stuff imported
import httplib2
import os
import sys

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
from oauth2client.tools import argparser, run_flow
import time

#discord stuff imported
import discord #gets the discord and asyncio librarys
import asyncio

##problems
#unsure what will happen in a headless enviroment if the oauth hasnt been set
##if the token and you input a invalid token the first time it will continue to say invalid token for tokens that are even valid

##ideas
#possiblity of use of file io to get the information of the client token for discord and stuff.
#use regex to help format the chat (honestly not needed)

##youtube chat porition
#this will handle getting chat from youtube which will then be pushed to discord

#!/usr/bin/python

####variables
config = {"channelName": "", "pageToken": "", "serverName": "", "discordToken": "","discordToYoutubeFormating": "", "youtubeToDiscordFormatting":"","discordToYoutube":True,"youtubeToDiscord":True}

botName = "none"

botUserID = "empty"

youtube = "sTA0GTgFn5E"

firstRun = "off"

#used as global varibles and were defined before we start using them to avoid problems down the road
channelToUse = ""

# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
# the OAuth 2.0 information for this application, including its client_id and
# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
# the {{ Google Cloud Console }} at
# {{ https://cloud.google.com/console }}.
# Please ensure that you have enabled the YouTube Data API for your project.
# For more information about using OAuth2 to access the YouTube Data API, see:
#   https://developers.google.com/youtube/v3/guides/authentication
# For more information about the client_secrets.json file format, see:
#   https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
CLIENT_SECRETS_FILE = "client_secrets.json"

# This OAuth 2.0 access scope allows for full read/write access to the
# authenticated user's account.
YOUTUBE_READ_WRITE_SCOPE = "https://www.googleapis.com/auth/youtube"
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"

# This variable defines a message to display if the CLIENT_SECRETS_FILE is
# missing.
MISSING_CLIENT_SECRETS_MESSAGE = """
WARNING: Please configure OAuth 2.0
To make this sample run you will need to populate the client_secrets.json file
found at:
   %s
with information from the {{ Cloud Console }}
{{ https://cloud.google.com/console }}
For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
                                   CLIENT_SECRETS_FILE))

def get_authenticated_service(args):
  flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
    scope=YOUTUBE_READ_WRITE_SCOPE,
    message=MISSING_CLIENT_SECRETS_MESSAGE)

  storage = Storage("%s-oauth2.json" % sys.argv[0])
  credentials = storage.get()

  if credentials is None or credentials.invalid:
    credentials = run_flow(flow, storage, args)

  return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
    http=credentials.authorize(httplib2.Http()))

# Retrieve a list of the liveStream resources associated with the currently
# authenticated user's channel.

def getLiveId(youtube): #this gets the live chat id
  global liveChatId,botUserID #pulls in the bots livechatid and botuserid for further saving and modifying

  list_streams_request = youtube.liveBroadcasts().list( #checks for the live chat id through this
    part="snippet", #this is what we look through to get the live chat id
    broadcastStatus="all", #we need both of these to get the live chat id
    broadcastType="all"
  ).execute() #executes it so its not just some object
  liveChatId = list_streams_request["items"][0]["snippet"]["liveChatId"]#sifts through the output to get the live chat id and saves it
  botUserID = list_streams_request["items"][1]["snippet"]["channelId"] #saves the bots channel user id that we will use as a identifier
  print("liveID {0}".format(liveChatId)) #print the live chat id

async def listChat(youtube):
  global pageToken #pulls in the page token
  global liveChatId #pulls in the liveChatID
  global botUserID #pulls in the bots channel ID
  global config
  try:
    continuation = True
    try:
      list_chatmessages = youtube.liveChatMessages().list( #lists the chat messages
        part="id,snippet,authorDetails", #gets the author details needed and the snippet all of which giving me the message and username
        liveChatId=liveChatId,
        maxResults=200,
        pageToken=config["pageToken"] #gives the previous token so it loads a new section of the chat
      ).execute() #executes it so its not just some object

      config["pageToken"] = list_chatmessages["nextPageToken"] #page token for next use
    except HttpError:
      continuation = False 
    #print(list_chatmessages)

    if continuation == True:
      msgCheckRegex = re.compile(r'(:)') #setup for if we happen to need this it should never change either way
      for temp in list_chatmessages["items"]: #goes through all the stuff in the list messages list
        message = temp["snippet"]["displayMessage"] #gets the display message
        username = temp["authorDetails"]["displayName"] #gets the users name
        userID = temp["authorDetails"]["channelId"]
        if message != "" and username != "" and config["youtubeToDiscord"] == True: #this makes sure that the message and username slot arent empty before putting this to the discord chat
            print(temp)
            fileSave("youtubeMsgJson.json", temp)
            if userID != botUserID:
              print(config["youtubeToDiscordFormatting"].format(username,message))
              msg = (config["youtubeToDiscordFormatting"].format(username,message))
              await channelToUse.send(msg)
            elif userID == botUserID: #if the userId is the bots then check the message to see if the bot sent it.
                try:
                  msgCheckComplete = msgCheckRegex.search(message) #checks the message against the previously created regex for ":"
                  if msgCheckComplete.group(1) != ":": #if its this then go and send the message as normal
                    print(config["youtubeToDiscordFormatting"].format(username,message))
                    msg = (config["youtubeToDiscordFormatting"].format(username,message))
                    print("sending")
                    print(botUserID)
                    await channelToUse.send(msg)
                except AttributeError as error:
                  msg = (config["youtubeToDiscordFormatting"].format(username,message))
                  print("sending")
                  print(botUserID)
                  await channelToUse.send(msg)
  except  ConnectionResetError:
    login()

    print("Youtube Connection Error... Reconnecting")

async def sendLiveChat(msg): #sends messages to youtube live chat
   list_chatmessages_inset = youtube.liveChatMessages().insert(
     part = "snippet",
     body = dict (
        snippet = dict(
           liveChatId = liveChatId,
           type = "textMessageEvent",
           textMessageDetails = dict(
               messageText = msg
           )
         )
      )
   )
   list_chatmessages_inset.execute()
   #print(list_chatmessages_inset.execute()) #debug for sending live chat messages

def login():
  global youtube
  args = argparser.parse_args()
  youtube = get_authenticated_service(args) #authenticates the api and saves it to youtube
  getLiveId(youtube)

if __name__ == "__main__":
    login()
    #args = argparser.parse_args()

    #youtube = get_authenticated_service(args) #authenticates the api and saves it to youtube
    #getLiveId(youtube)

##discord portion of the bot
#this will be the main code

client = discord.Client() #sets this to just client for reasons cuz y not? (didnt have to be done like this honestly could of been just running discord.Client().)

async def discordSendMsg(msg): #this is for sending messages to discord
    global config
    global channelToUse #pulls in t{0} : {1}".format(author,msg)he global variable
    await channelToUse.send(msg) #sends the message to the channel specified in the beginning

@client.event
async def on_ready(): #when the discord api has logged in and is ready then this even is fired
    if firstRun == "off":
        #these 2 variables are used to keep track of what channel is thre real channel to use when sending messages to discord
        global config , botName
        global channelToUse #this is where we save the channel information (i think its a class)
        global channelUsed #this is the channel name we are looking for
        #this is just to show what the name of the bot is and the id
        print('Logged in as') ##these things could be changed a little bit here
        print(client.user.name+ "#" + client.user.discriminator)
        botName = client.user.name + "#" + client.user.discriminator #gets and saves the bots name and discord tag
        print(client.user.id)
        for guild in client.guilds: #this sifts through all the bots servers and gets the channel we want
            #should probly add a check in for the server in here im guessing
            for channel in guild.channels:
                if channel.name == config["channelName"] and str(channel.type) == "text": #checks if the channel name is what we want and that its a text channel
                    channelToUse = channel #saves the channel that we wanna use since we found it

        await youtubeChatImport() #runs the youtube chat import code
    else:
        await getFirstRunInfo()

async def youtubeChatImport(): #this is used to pull the from youtube to discord
    i = 0
    while True:
        i += 1
        try:
          await listChat(youtube) #checks the youtube chat
        except:
          pass
        if i == 8:
            fileSave("config.json", config)
            i = 0
        await asyncio.sleep(5) #this works

@client.event
async def on_message(message): #waits for the discord message event and pulls it somewhere
    global config
    global channelToUse #pulls in the global variable
    if firstRun == "off":
        if str(channelToUse.name) == str(message.channel) and str(message.author.name) != botName and str(message.author.name) != client.user.name and str(message.author.name) != client.user.discriminator and config["discordToYoutube"] == True:
            print(config["discordToYoutubeFormating"].format(message.author,message.content)) #prints this to the screen
            await sendLiveChat(config["discordToYoutubeFormating"].format(message.author.display_name,message.content)) #prints this to the screen

##file load and save stuff

def fileSave(filename, config):
    print("Saving")
    f = open(filename, 'w') #opens the file your saving to with write permissions
    f.write(json.dumps(config) + "\n") #writes the string to a file
    f.close() #closes the file io

def fileLoad():
    f = open("config.json", 'r') #opens the file your saving to with read permissions
    config = ""
    for line in f: #gets the information from the file
        config = json.loads(line) #this will unserialize the table
    return config

##first run stuff

def getToken(): #gets the token
    global config
    realToken = "false" #this is just for the while loop
    while realToken == "false":
        config["discordToken"] = input("Discord Token: ") #gets the user input
        try:
            client.run(config["discordToken"]) #atempts to run it and if it fails then execute the next bit of code if not then save it and go on
        except:
            print("Please enter a valid token")
            sys.exit(0) #this is a work around for the bug that causes the code not think the discord token is valid even tho it is after the first time of it being invalid
        else:
            realToken = "true"

async def getFirstRunInfo():
    global config
    print('Logged in as') ##these things could be changed a little bit here
    print(client.user.name)
    print(client.user.id)
    while config["serverName"] == "":
        for guild in client.guilds: #this sifts through all the bots servers and gets the channel we want
            print(guild.name)
            if input("If this is the server you want type yes if not hit enter: ") == "yes":
                config["serverName"] = guild.name
                break
    while config["channelName"] == "":
        for guild in client.guilds: #this sifts through all the bots servers and gets the channel we want
            #should probly add a check in for the server in here im guessing
            #print(server.name)
            for channel in guild.channels:
                if str(channel.type) == "text":
                    print(channel.name)
                    if input("If this is the channel you want type yes if not hit enter: ") == "yes":
                        config["channelName"] = channel.name
                        break
    while config["youtubeToDiscordFormatting"] == "": #fills the youtube to discord formating
        config["youtubeToDiscordFormatting"] = input("""Please enter the chat formatting for chat coming from youtube to go to discord.
{0} is the placeholder for the username
{1} is the placeholder for the message
Ex. "{0} : {1}: """)
    while config["discordToYoutubeFormating"] == "": #fills the discord to youtube formating
        config["discordToYoutubeFormating"] = input("""Please enter the chat formatting for chat coming from discord to go to youtube.
{0} is the placeholder for the username
{1} is the placeholder for the message
Ex. "{0} : {1}": """)
    print("Configuration complete")
    fileSave("config.json", config) #saves the file
    print("Please run the command normally to run the bot")
    await client.close()

if os.path.isfile("config.json") == False:#checks if the file exists and if it doesnt then we go to creating it
    print("Config missing. This may mean this is your first time setting this up")
    firstRun = "on"
else:
    config = fileLoad() #if it exists try to load it
if firstRun == "on":
    config = {"channelName": "", "pageToken": "", "serverName": "", "discordToken": "","discordToYoutubeFormating": "", "youtubeToDiscordFormatting":"","youtubeToDiscord": True, "discordToYoutube": True}
    getToken()

def discordStart(token):
  while True:
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(client.start(token))
    except (discord.ConnectionClosed, discord.GatewayNotFound) as error:
        loop.run_until_complete(client.logout())
        loop.close()
        discordStart(token)
        print("Client Connection Lost")
        # cancel all tasks lingering
    except KeyboardInterrupt:
        loop.run_until_complete(client.logout())
    finally:
        print("Client Closed")
        loop.close()
    print("Reconnecting")

discordStart(config["discordToken"])
popcorn9499 commented 3 years ago

without a log as to what happened I would only be speculating. However I really haven't used this code since 2017 when I implemented it for https://github.com/popcorn9499/chatBot however youtube changed the api limits to something rather small making this entire program almost useless since it will hit the api limits in a few hours with 2 or so people chatting. maybe less maybe more depending on the amount of messages.

Your best bet would be to setup something like selenium to interact with the webpage for live chat directly and parse that to pull it to discord and vis versa. however I'm sure that would break some tos somewhere.

Anyways I can give a look at what's wrong if you send me the output from the program instead of just the code. otherwise its all speculation

jayKeegan commented 3 years ago

Here is the console output:


liveID KicKGFVDakNCejNKSy16bm1nbmVHcUhaNmk1ZxILTlFVWjVPR1Y3dzQ
Logged in as
Rocket Launches Today#3997
789632676926717962
popcorn9499 commented 3 years ago

I see nothing in that log that indicates that it even noticed a message at all let alone tried to mirror it to the other service. try to send a message on both youtube and discord if you haven't already. you must do it when the application is running. Any please give youtube a good 5-30 seconds to propagate to discord as you have to hit the api manually and each request takes some of your google api quota which is in very short supply. if you have tried this I really don't know what is wrong.

jayKeegan commented 3 years ago

There was an active conversation in the YouTube chat at the time, therefor messages should have been coming through. Are there any more logs available to figure out what's going on?

popcorn9499 commented 3 years ago

one thing I just realized might explain at least some of it. could you send me a copy of the config.json I don't need the discordToken or anything confidential but I do need to see the rest of it.

otherwise its possible the discord api changed as well since I wrote this bot and that is possibly to blame for it not even detecting messages from discord. But honestly its possibly configuration error. However since I havent used this code really since 2017 I am not really sure. Chatbot was sorta the better spinoff to this so it may be in your best interest to setup that

jayKeegan commented 3 years ago

{"channelName": "\ud83d\udcac-general", "pageToken": "", "serverName": "Jay's test server", "discordToken": "TOKEN REMOVED", "discordToYoutubeFormating": "\"{0} - {1}\"", "youtubeToDiscordFormatting": "\"{0} - {1}\"", "youtubeToDiscord": true, "discordToYoutube": false}````
popcorn9499 commented 3 years ago

I am assuming you already know but you need discordToYoutube set to true to allow discord to youtube communication. However you could try enabling it and seeing how that works. It is also possible the what I believe is unicode in the channel name is causing issues with the bot. I am not sure if either of those is the case but if you could try stuff in this order.

Try setting discordToYoutube to true and sending a message in discord monitoring youtube chat to see if it appears. If it doesnt look in the bot console it should show something in the console. Please send me a copy of this log regardless at this step. if it doesnt work go onto attempt 2.

Now onto attempt 2. if it still isnt working then try making a channel without unicode or special characters. dashes should be fine and this will just indicate where the issue is specifically. If you could also send a copy of the console at this step I would greatly appreciate it.

jayKeegan commented 3 years ago

Discord to YT error:

Ignoring exception in on_message Traceback (most recent call last): File "C:\Users\Jay\AppData\Local\Programs\Python\Python39\lib\site-packages\discord\client.py", line 343, in _run_event await coro(*args, **kwargs) File "C:\Users\Jay\Documents\GitHub\sync-ytchat-to-discord\bot.py", line 259, in on_message await sendLiveChat(config["discordToYoutubeFormating"].format(message.author.display_name,message.content)) #prints this to the screen File "C:\Users\Jay\Documents\GitHub\sync-ytchat-to-discord\bot.py", line 186, in sendLiveChat list_chatmessages_inset.execute() File "C:\Users\Jay\AppData\Local\Programs\Python\Python39\lib\site-packages\googleapiclient\_helpers.py", line 134, in positional_wrapper return wrapped(*args, **kwargs) File "C:\Users\Jay\AppData\Local\Programs\Python\Python39\lib\site-packages\googleapiclient\http.py", line 935, in execute raise HttpError(resp, content, uri=self.uri) googleapiclient.errors.HttpError: <HttpError 403 when requesting https://youtube.googleapis.com/youtube/v3/liveChat/messages?part=snippet&alt=json returned "The specified live chat is no longer live.". Details: "[{'message': 'The specified live chat is no longer live.', 'domain': 'youtube.liveChat', 'reason': 'liveChatEnded'}]">

jayKeegan commented 3 years ago

Looks like I got it working, thanks for your help!

popcorn9499 commented 3 years ago

Looks like I got it working, thanks for your help!

If you dont mind me asking. what was the solution?

jayKeegan commented 3 years ago

Looks like I got it working, thanks for your help!

If you dont mind me asking. what was the solution?

Looks like the unicode character in the channel name was messing it up, I honestly have no idea how I fixed the discord to YT error haha

popcorn9499 commented 3 years ago

Looks like I got it working, thanks for your help!

If you dont mind me asking. what was the solution?

Looks like the unicode character in the channel name was messing it up, I honestly have no idea how I fixed the discord to YT error haha

ah ok thank you. The error you saw was something specific to the google/youtube api specifically

<HttpError 403 when requesting https://youtube.googleapis.com/youtube/v3/liveChat/messages?part=snippet&alt=json returned "The specified live chat is no longer live.". Details: "[{'message': 'The specified live chat is no longer live.', 'domain': 'youtube.liveChat', 'reason': 'liveChatEnded'}]">

Specifically something about Live chat has ended. Not sure what you did honestly to change that but hey Im glad its working.