Python asyncio with Slack bot

2019-05-16 10:06发布

问题:

I'm trying to make a simple Slack bot using asyncio, largely using the example here for the asyncio part and here for the Slack bot part.

Both the examples work on their own, but when I put them together it seems my loop doesn't loop: it goes through once and then dies. If info is a list of length equal to 1, which happens when a message is typed in a chat room with the bot in it, the coroutine is supposed to be triggered, but it never is. (All the coroutine is trying to do right now is print the message, and if the message contains "/time", it gets the bot to print the time in the chat room it was asked in). Keyboard interrupt also doesn't work, I have to close the command prompt every time.

Here is my code:

import asyncio
from slackclient import SlackClient
import time, datetime as dt

token = "MY TOKEN"
sc = SlackClient(token)

@asyncio.coroutine
def read_text(info):
    if 'text' in info[0]:
        print(info[0]['text'])
        if r'/time' in info[0]['text']:
            print(info)
            resp = 'The time is ' + dt.datetime.strftime(dt.datetime.now(),'%H:%M:%S')
            print(resp)
            chan = info[0]['channel']
            sc.rtm_send_message(chan, resp)


loop = asyncio.get_event_loop()
try:
    sc.rtm_connect()
    info = sc.rtm_read()
    if len(info) == 1:
        asyncio.async(read_text(info))
    loop.run_forever()

except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

I think it's the loop part that's broken, since it never seems to get to the coroutine. So maybe a shorter way of asking this question is what is it about my try: statement that prevents it from looping like in the asyncio example I followed? Is there something about sc.rtm_connect() that it doesn't like?

I'm new to asyncio, so I'm probably doing something stupid. Is this even the best way to try and go about this? Ultimately I want the bot to do some things that take quite a while to compute, and I'd like it to remain responsive in that time, so I think I need to use asyncio or threads in some variety, but I'm open to better suggestions.

Thanks a lot, Alex

回答1:

I changed it to the following and it worked:

import asyncio
from slackclient import SlackClient
import time, datetime as dt

token = "MY TOKEN"    
sc = SlackClient(token)

@asyncio.coroutine
def listen():
    yield from asyncio.sleep(1)
    x = sc.rtm_connect()
    info = sc.rtm_read()
    if len(info) == 1:
        if 'text' in info[0]:
            print(info[0]['text'])
            if r'/time' in info[0]['text']:
                print(info)
                resp = 'The time is ' + dt.datetime.strftime(dt.datetime.now(),'%H:%M:%S')
                print(resp)
                chan = info[0]['channel']
                sc.rtm_send_message(chan, resp)

    asyncio.async(listen())


loop = asyncio.get_event_loop()
try:
    asyncio.async(listen())
    loop.run_forever()

except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

Not entirely sure why that fixes it, but the key things I changed were putting the sc.rtm_connect() call in the coroutine and making it x = sc.rtm_connect(). I also call the listen() function from itself at the end, which appears to be what makes it loop forever, since the bot doesn't respond if I take it out. I don't know if this is the way this sort of thing is supposed to be set up, but it does appear to continue to accept commands while it's processing earlier commands, my slack chat looks like this:

me [12:21 AM] 
/time

[12:21] 
/time

[12:21] 
/time

[12:21] 
/time

testbotBOT [12:21 AM] 
The time is 00:21:11

[12:21] 
The time is 00:21:14

[12:21] 
The time is 00:21:16

[12:21] 
The time is 00:21:19

Note that it doesn't miss any of my /time requests, which it would if it weren't doing this stuff asynchronously. Also, if anyone is trying to replicate this you'll notice that slack brings up the built in command menu if you type "/". I got around this by typing a space in front.

Thanks for the help, please let me know if you know of a better way of doing this. It doesn't seem to be a very elegant solution, and the bot can't be restarted after I use the a cntrl-c keyboard interrupt to end it - it says

Task exception was never retrieved
future: <Task finished coro=<listen() done, defined at asynctest3.py:8> exception=AttributeError("'NoneType' object has no attribute 'recv'",)>
Traceback (most recent call last):
  File "C:\Users\Dell-F5\AppData\Local\Programs\Python\Python35-32\Lib\asyncio\tasks.py", line 239, in _step
    result = coro.send(None)
  File "asynctest3.py", line 13, in listen
    info = sc.rtm_read()
  File "C:\Users\Dell-F5\Envs\sbot\lib\site-packages\slackclient\_client.py", line 39, in rtm_read
    json_data = self.server.websocket_safe_read()
  File "C:\Users\Dell-F5\Envs\sbot\lib\site-packages\slackclient\_server.py", line 110, in websocket_safe_read
    data += "{0}\n".format(self.websocket.recv())
AttributeError: 'NoneType' object has no attribute 'recv'

Which I guess means it's not closing the websockets nicely. Anyway, that's just an annoyance, at least the main problem is fixed.

Alex



回答2:

Making blocking IO calls inside a coroutine defeat the very purpose of using asyncio (e.g. info = sc.rtm_read()). If you don't have a choice, use loop.run_in_executor to run the blocking call in a different thread. Careful though, some extra locking might be needed.

However, it seems there's a few asyncio-based slack client libraries you could use instead:

  • slacker-asyncio - fork of slacker, based on aiohttp
  • butterfield - based on slacker and websockets

EDIT: Butterfield uses the Slack real-time messaging API. It even provides an echo bot example that looks very much like what you're trying to achieve:

import asyncio
from butterfield import Bot

@asyncio.coroutine
def echo(bot, message):
    yield from bot.post(
        message['channel'],
        message['text']
    )

bot = Bot('slack-bot-key')
bot.listen(echo)
butterfield.run(bot)