使用 tweepy 和 discord.py 将推文发布到特定频道

2022-01-15 00:00:00 python tweepy discord discord.py twitter

问题描述

因此,我正在尝试结合使用教程、文档和在线示例来自学一点 Python,以构建一个 Discord 机器人,该机器人实现了一个或多个现有机器人的某些功能.其中一个功能是将一组 Twitter 帐户中的(新)推文发布到我的 Discord 服务器上的特定频道.我发现了几段代码,我拼凑在一起从 Twitter 流中读取并调整"了这些代码.在这里和那里做一些事情来尝试实现这一点.

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.

但是,我一直遇到一个问题,实际上是让 on_status 代码正确执行.我尝试了多种方法来使其正常工作,但我尝试的所有方法都会导致某种错误.以下是我一直在测试的最新迭代中的相关代码(已编辑):

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)

当我将推文发布到我的测试帐户的时间线时,channel.send() 方法会生成以下消息:

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

我理解消息 - channel.send() 方法是异步方法,但 on_status() 事件处理程序是同步方法,我不能await channel.send() 里面 on_status() - 但我不知道如何使它工作.我试图使 on_status() 方法成为异步方法:

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:

我发现了问题,How do I async on_status from tweepy? 并按照那里有链接,但我没有看到任何让我知道我的编码错误的东西.

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.

通过额外的研究,我还尝试使用 asyncio 库中的一些调用来进行调用:

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()

不幸的是,当它尝试执行 loop=asyncio.get_event_loop() 时,这会导致未处理的异常,表明 线程 'Thread-6' 中没有当前事件循环.code>(所以它甚至不会使用 run_until_complete() 方法来查看 that 是否会工作,尽管我目前并不完全乐观).

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).

我意识到现有的 Discord 机器人(例如 MEE6)已经可以完成我在这里尝试做的事情,但我希望能够让这个机器人成为我自己的"机器人.同时自学一些关于 Python 的知识.我可能在这里忽略了一些简单的事情,但是我无法在 API 文档中找到任何似乎指向我的 tweepydiscord.py在正确的方向上,我的 Google-fu 显然不是那么强大.到目前为止,我能够找到的所有示例似乎都已过时,因为它们似乎引用了已弃用的方法和其中一个或两个库的旧版本.还有其他事情我仍然需要弄清楚如何去做(例如,让 @TwitterHandle 正确填充嵌入),但我必须让它能够阅读并推送推文,然后我才能担心到达那一点.

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.

Visual Studio 2017 CE

Visual Studio 2017 CE

Python 3.6

discord.py 1.3.3

discord.py 1.3.3

tweepy 3.8.0

tweepy 3.8.0

所以,为了向自己证明我的 TweetListener 类确实有效,我继续在 on_status 方法中注释掉所有与 Discord 的交互(从 for Guild in MyBot.guilds 到方法末尾的所有内容),然后添加了一个简单的 print(TweetText).我再次运行代码,登录到我为这个项目拥有的测试 Twitter 帐户,并发布了更新.检查控制台,它正确地输出了我的推文的 status.text,正如我所期望的那样.正如我所想,我在这里遇到的唯一问题是尝试将该文本 send 到 Discord.

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.

我还尝试在同步模式而不是异步模式下初始化 tweepy.Stream.filter - NewStream.filter(follow=['USER IDS']).这只会导致我的应用程序基本上挂起".在启动时.我意识到这是由于它在代码中的位置,所以我将 TweetListener 初始化的所有三行都移到了 on_ready 事件方法中.我将 Discord 代码注释掉并进行了测试,它再次工作"了.因为它将另一条测试推文的 status.text 打印到控制台.但是,机器人不会响应任何其他内容(我假设仍在等待"流).

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).

即便如此,只是为了看看它是否会有所作为,我继续取消了 Discord 交互代码的注释并再次尝试.结果与我最初的结果相同,只是这次 Discord 机器人不会响应频道中的活动(在其他地方有几个超级简单的 @bot.command 声明代码),这又一次让我回到原来的代码,遇到同样的问题.

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.

我确信有更好的方法来编写整个代码.正如我上面所说,我现在才开始玩 Python 并且还在学习语法规则等.即便如此,我仍然不确定自己做错了什么和/或忽略了这使我无法完成我认为相当简单的任务.

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.

我回去又找到了一些 asyncio 示例,并在 on_status 中的 Discord 交互中尝试了另一个迭代:

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))

这一次我得到了一个 不同 未处理的异常,而不是我以前得到的.run_until_complete 行在获取新推文时发生错误:

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

我考虑了这个进展,但我显然还没有完全到那里,所以回到谷歌,我找到了关于 asgiref 库及其 sync_to_async 的信息>async_to_sync 方法,所以我想我会尝试一下.

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)

不幸的是,我得到了与 asyncio 版本相同的未处理异常.我觉得我正处于弄清楚这一点的边缘,但它根本没有点击".还没有.

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.

好的,所以在寻找有关 Timeout context manager 异常的信息后,我偶然发现了另一个 SO 问题,这给了我一线希望.RuntimeError: Timeout context manager should be used inside a task 上的这个答案让我再次回到使用 asyncio 并在使用 asyncio.run_coroutine_threadsafe 方法提供有用的建议之前,对 OP 问题背后的原因进行简要但描述性的解释.查看推荐的代码,这可以帮助我有效地启动 sync->async 方法通信是有道理的.我实现了建议的更改(为运行机器人的 Thread 对象创建了一个全局变量,添加了一个在该循环中生成机器人的启动"方法,然后在 on_status 将它们整合在一起.

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.

好消息"是我在发布推文时不再收到 any 错误.此外,测试 bot 命令似乎也可以正常工作.坏消息是它仍然没有将消息发送到频道.因为它没有产生任何错误,所以不知道消息在哪里结束.

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.


解决方案

我遇到了和你一样的问题,并从谷歌获得了确切的链接并尝试了所有这些链接,但正如你所提到的,没有任何效果.

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.

所以,经过大量尝试和修改后,我意识到,我可以将主循环传递给 tweepy 侦听器并使用 run_coroutine_threadsafe 执行异步函数.

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.

这是我的代码的要点:

听者:

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()

不和谐的客户:

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)

希望这会有所帮助.

相关文章