5
votes

So, I'm trying to teach myself a bit of Python using a combination of tutorials, documentation, and online examples to build a Discord bot that implements some features of one or more existing bots. One of those features is posting (new) tweets from a certain set of Twitter accounts to a specific channel on my Discord server. I've found several bits and pieces of code that I've cobbled together to read from a Twitter stream and "tweaked" a few things here and there to try to accomplish this.

However, I keep running into an issue with actually getting the on_status code to execute properly. I've tried a variety of ways to get it working, but everything I've tried results in some sort of error. Below is the pertinent code (redacted) from the most recent iteration that I've been testing:

import discord
import tweepy

from discord.ext import commands
from tweepy import Stream
from tweepy.streaming import StreamListener

class TweetListener(StreamListener):
    def on_status(self, status):
        if status.in_reply_to_status_id is None:
            TweetText = status.text

            for DGuild in MyBot.guilds:
                for DChannel in DGuild.text_channels:
                    if DChannel.name == 'testing':
                        TwitterEmbed = discord.Embed(title='New Tweet', description='New Tweet from my timeline.', color=0xFF0000)
                        TwitterEmbed.set_author(name='@TwitterHandle', icon_url=bot.user.default_avatar_url)
                        DChannel.send(TweetText, embed = TwitterEmbed)

DISCORD_TOKEN = 'dtoken'
TWITTER_CONSUMER_KEY = 'ckey'
TWITTER_CONSUMER_SECRET = 'csecret'
TWITTER_ACCESS_TOKEN = 'atoken'
TWITTER_ACCESS_SECRET = 'asecret'

MyBot = commands.Bot(command_prefix='!', description='This is a testing bot')
TwitterAuth = tweepy.OAuthHandler(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET)
TwitterAuth.set_access_token(TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET)
TweetAPI = tweepy.API(TwitterAuth)
NewListener = TweetListener()
NewStream = tweepy.Stream(auth=TweetAPI.auth, listener=NewListener)
NewStream.filter(follow=['USER IDS HERE'], is_async=True)

@bot.event
async def on_ready():
    print(MyBot.user.name,'has successfully logged in ('+str(MyBot.user.id)+')')
    print('Ready to respond')

MyBot.run(DISCORD_TOKEN)

The channel.send() method generates the following message when I post a tweet to my testing account's timeline:

RuntimeWarning: coroutine 'Messageable.send' was never awaited

I understand the message - the channel.send() method is an asynchronous method, but the on_status() event handler is a synchronous method and I can't await channel.send() inside on_status() - but I can't figure out how to make it work. I tried to make the on_status() method an asynchronous method:

    async def on_status(self, status):
        <same code for checking the tweet and finding the channel>
        await DChannel.send(TweetText, embed = TwitterEmbed)

but this always resulted in a similar warning:

RuntimeWarning: coroutine 'TweetListener.on_status' was never awaited
if self.on_status(status) is False:

I found the question, How do I async on_status from tweepy? and followed the links there, but I didn't see anything that clued me in to whatever my coding error is.

Through additional research, I also tried using some calls from the asyncio library to make the call:

    #channel.send(message, embed = TwitterEmbed)
    #---ASYNCIO TEST---
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.gather(DChannel.send(embed=TwitterEmbed)))
    loop.close()

Unfortunately, this results in an unhandled exception when it attempts to execute loop=asyncio.get_event_loop() indicating There is no current event loop in thread 'Thread-6'. (so it won't even get to the run_until_complete() method to see if that will work, although I'm not exactly optimistic at this point).

I realize there are existing Discord bots like MEE6 that can already do what I'm trying to do here, but I'd like to be able to make this bot "my own" while teaching myself a little about Python. I'm probably overlooking something simple here, but I've not been able to find anything in the API documentation for either tweepy or discord.py that seems to be pointing me in the correct direction, and my Google-fu is apparently not that strong. All of the examples I've been able to find so far appear to be out-of-date as they seem to refer to deprecated methods and older versions of either or both libraries. There are other things that I still have to figure out how to do as well (e.g., get the @TwitterHandle to correctly populate the embed), but I've got to get it able to read and push the tweet before I can worry about getting to that point.


ENVIRONMENT INFO

Visual Studio 2017 CE

Python 3.6

discord.py 1.3.3

tweepy 3.8.0


UPDATE: ADDITIONAL TESTING

So, just to prove to myself that my TweetListener class is actually working, I went ahead and commented out all of the interaction with Discord in the on_status method (everything from for Guild in MyBot.guilds to the end of the method), then added a simple print(TweetText). I ran the code again, logged in to the testing Twitter account I have for this project, and posted an update. Checking the console, it correctly output the status.text of my tweet, as I expected it to. As I thought, the only issue I'm having here is trying to send that text to Discord.

I also tried initializing the tweepy.Stream.filter in synchronous mode instead of asynchronous - NewStream.filter(follow=['USER IDS']). This only resulted in my application basically "hanging" on startup. I realized this was due to its position in the code, so I moved all three lines of the TweetListener initialization to the on_ready event method. I left the Discord code commented out and tested, and it again "worked" in that it printed the status.text of another test tweet to the console. However, the bot won't respond to anything else (still "waiting" on the stream, I assume).

Even so, just to see if it would make any difference, I went ahead and uncommented the Discord interaction code and tried again. The result was the same as I had initially, only this time the Discord bot won't respond to activity in the channel (there are a couple of super simple @bot.command declarations elsewhere in the code), which once again basically leads me back to my original code with the same issue.

I'm certain there are better ways to code this entire thing. As I said above, I'm just now starting to play around with Python and still learning syntax rules and such. Even so, I'm still not sure what I'm doing wrong and/or overlooking that's preventing me from achieving what I had thought would be a fairly simple task.


MORE TESTING

I went back and found some more asyncio examples and tried another iteration in the Discord interaction in on_status:

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    result=loop.run_until_complete(DChannel.send(embed=TwitterEmbed))

This time I got a different unhandled exception than I've gotten before. The error occurs on the run_until_complete line when it picks up a new tweet:

Timeout context manager should be used inside a task

I consider this progress, but I'm obviously still not quite there, so back to Google and I found information on the asgiref library with its sync_to_async and async_to_sync methods, so I figured I'd give that a try.

    asgiref.sync.async_to_sync(DChannel.send)(embed=TwitterEmbed)

Unfortunately, I get the same unhandled exception as with the asyncio version. I feel like I'm right on the edge of figuring this out, but it simply hasn't "clicked" yet.


ANOTHER ROUND

Okay, so after looking for information about the Timeout context manager exception I was getting above, I stumbled across another SO question that has given me a tiny glimmer of hope. This answer on RuntimeError: Timeout context manager should be used inside a task once again takes me back to using asyncio and gives a brief but descriptive explanation of the reason behind the OP's issue before providing a useful suggestion using the asyncio.run_coroutine_threadsafe method. Looking at the recommended code, it made sense that this could help me initiate the sync->async method communication effectively. I implemented the suggested changes (created a global variable for a Thread object on which to run the bot, added a "startup" method that generated the bot in that loop, then changed the Discord interaction in on_status to bring it all together.

The "good news" is that I'm no longer getting any errors when I post a tweet. Also, testing the bot command seems to work just fine as well. The bad news is that it still didn't send the message to the channel. Since it didn't produce any errors, there's no telling where the message ended up.

1

1 Answers

2
votes

I went through the same problem as you have, and got the exact links from google and tried all of them but as you have mentioned nothing worked.

So, after lots of trying and tinkering I realised, I could pass the main loop to the tweepy listener and execute async functions with run_coroutine_threadsafe.

Here is the gist of my code:

the listener:

class EpicListener(tweepy.StreamListener):
    def __init__(self, discord, loop, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.discord = discord # this is just a function which sends a message to a channel
        self.loop = loop # this is the loop of discord client

    def on_status(self, status):
        self.send_message(status._json)

    def send_message(self, msg):
        # Submit the coroutine to a given loop
        future = asyncio.run_coroutine_threadsafe(self.discord(msg), self.loop)
        # Wait for the result with an optional timeout argument
        future.result()

the discord client:

class MyClient(discord.Client):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    async def on_ready(self):
        myStream = tweepy.Stream(
                    auth=api.auth, listener=EpicListener(discord=self.sendtwitter, loop=asyncio.get_event_loop())
                )
        myStream.filter(follow=['mohitwr'], is_async=True)
        print(myStream)

Hope this helps.