Expose discord bot to API (Flask, FASTAPI) - python

I'm building a discord bot to take commands from multiple systems and programs. I'm wanting to expose certain actions of my discord bot to REST endpoints and then execute said actions in one spot.
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from discord.ext import commands
app = FastAPI()
TOKEN = 'MY_TOKEN'
bot = commands.Bot(command_prefix='>')
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
#app.get("/")
def hello():
return {"message":"Hello"}
#app.post("/items/")
async def create_item(item: Item):
await send_message()
return item
#bot.event
async def on_ready():
print(f'{bot.user.name} has connected to Discord!')
async def send_message():
user = await bot.fetch_user(USER_ID)
await user.send('๐Ÿ‘€')
if __name__ == "__main__":
bot.run('BOT_TOKEN')
uvicorn.run(app, host='0.0.0.0')
When I try to run this, I'm only seeing the bot active. I'm a little newer to python but a veteran programmer. Is this due to python's "lack" of multithreading? Or port usage?
The end goal is to call the "/items/" endpoint and see a message on discord sent to me
EDIT
I tried all the answers and coming up with some of my own. The problem is multi-threading. I got frustrated with it and ended up just moving this piece to Node.js. It doesn't technically fulfill this question but was far easier than navigating python multithreading.
server.js:
var express = require('express');
var app = express();
const Discord = require('discord.js');
const client = new Discord.Client();
app.get('/listUsers', function (req, res) {
dm_user();
res.send('hello');
})
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
async function dm_user(id){
var my_user = await client.users.fetch('USER_ID');
console.log(my_user);
}
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
client.login('TOKEN');
})

According to the discord.py docs bot.run() is "A blocking call that abstracts away the event loop initialisation from you." and further they said if we want more control over the loop we could use start() coroutine instead of run(). So now we should create a task for calling this coroutine and we know discord.py and FastAPI all are asynchronous applications. For starting a FastAPI app you need an ASGI server to handle it. In this case, we're using Uvicorn. So far we have run FastAPI app, now we need to start our discord bot. According to FastAPI docs we could use startup/shutdown event, for calling bot.start() coroutine before the main API starts.
Here is an example of an app which has an API endpoint for sending a message to a discord's user:
import asyncio
import discord
import uvicorn
from config import TOKEN, USER_ID
from fastapi import FastAPI
app = FastAPI()
bot = discord.Client()
#app.on_event("startup")
async def startup_event(): #this fucntion will run before the main API starts
asyncio.create_task(bot.start(TOKEN))
await asyncio.sleep(4) #optional sleep for established connection with discord
print(f"{bot.user} has connected to Discord!")
#app.get("/")
async def root(msg: str): #API endpoint for sending a message to a discord's user
user = await send_message(msg)
return {"Message": f"'{msg}' sent to {user}"}
async def send_message(message):
user = await bot.fetch_user(USER_ID)
await user.send(message)
return user #for optional log in the response of endpoint
if __name__ == "__main__":
uvicorn.run(app, host="localhost", port=5000)
Tested with Python 3.7.4

You are not returning anything from your send_message function. Something like this should do good.
#app.post("/items/")
async def create_item(item: Item):
msg = await send_message()
return msg
async def send_message():
user = await bot.fetch_user(USER_ID)
return await user.send('๐Ÿ‘€')

Code bot.run(...) runs all time and it blocks next line which starts API. You would have to run one of them in separated thread or process.
I tried to run bot in thread
if __name__ == "__main__":
import threading
print('Starting bot')
t = threading.Thread(target=bot.start, args=(TOKEN,))
t.start()
print('Starting API')
uvicorn.run(app, host='0.0.0.0')
but it gives me message that bot should run in main thread.
But I found question Discord bot and bottle in the same time in Python and base on it I create code which works for me
if __name__ == "__main__":
import asyncio
print('Starting bot')
bot_app = bot.start(TOKEN)
bot_task = asyncio.ensure_future(bot_app)
print('Starting API')
uvicorn.run(app, host='0.0.0.0')
But I'm not sure if this is ellegant method because uvicorn runs ayncio indirectly.
Full version
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from discord.ext import commands
app = FastAPI()
#import os
#TOKEN = os.getenv("DISCORD_TOKEN")
TOKEN = 'MY_TOKEN'
bot = commands.Bot(command_prefix='>')
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
#app.get("/")
def hello():
return {"message":"Hello"}
#app.post("/items/")
async def create_item(item: Item):
await send_message()
return item
#bot.event
async def on_ready():
print(f'{bot.user.name} has connected to Discord!')
async def send_message():
user = await bot.fetch_user(USER_ID)
await user.send('๐Ÿ‘€')
if __name__ == "__main__":
import asyncio
print('Starting bot')
bot_app = bot.start(TOKEN)
bot_task = asyncio.ensure_future(bot_app)
print('Starting API')
uvicorn.run(app, host='0.0.0.0')

Related

Nextcord Slash Command Not Responding

I am trying to make a Discord bot with slash commands using Python's nextcord module. My invite link uses both bot and applications.commands scopes.
I've started with a ping command using examples I've found online. My code looks like this:
import nextcord, os
from dotenv import load_dotenv
from nextcord.ext import commands
from flask import Flask
from threading import Thread
load_dotenv()
app = Flask('')
#app.route('/')
def home() -> str:
return 'Running!'
def run() -> None:
app.run(port=int(os.environ.get('PORT', 33507)))
def keep_alive() -> None:
Thread(target=run).start()
TOKEN = os.environ['DISCORD_TOKEN']
description = '[bot name] [version]'
intents = nextcord.Intents.default()
intents.members = True
client = commands.Bot(command_prefix='/', description=description, intents=intents)
async def embed(title, description, reason) -> nextcord.Embed:
return nextcord.Embed(
title=title,
description=description,
color=0x00FF00
).set_author(
name='[bot name] [version]',
icon_url='[image link]'
).set_footer(
text=f'This message was sent because {reason}.'
)
#client.event
async def on_ready():
print(f'Logged in as {client.user} (ID: {client.user.id})')
await client.change_presence(activity=nextcord.Game(name='[version]'))
#client.slash_command(name='ping', description='Returns bot latency')
async def ping(interaction: nextcord.Interaction):
await client.process_application_commands(interaction)
await interaction.response.defer(with_message=True)
await interaction.followup.send(embed=embed(':ping_pong: Pong!', f'{client.latency * 100} ms', f'{interaction.user} used the "ping" command'))
if __name__ == '__main__':
keep_alive()
client.run(TOKEN)
I've used a function to return an embed object to use as message content.
When running /ping on Discord, it returns "[bot name] is thinking..." before eventually changing to "The application did not respond".
What am I doing wrong?
I've discovered the answer through this StackExchange post. I needed to use embed=await embed() as shown here:
await interaction.followup.send(embed=await embed(...))

Scheduled HTTP Request using FastAPI

Inside my FastAPI application, I would like to schedule an HTTP request to be made to check for new results (comparing to database) every X time interval. What would be the easiest way to accomplish this using httpx?
You can add an async task to the event loop during the startup event. This async task would check (and sleep) and store the result somewhere. In the below example, I've chosen to pass around a shared object using the app.state feature of FastAPI. This should give you enough pointers to implement your exact use case. I have commented out an example of dealing with https specifically.
from fastapi import FastAPI
import asyncio
class MySharedObject:
def __init__(self) -> None:
self.count = 0
async def timed_checker(obj: MySharedObject):
while True:
obj.count += 1
# async with httpx.AsyncClient() as client:
# r = await client.get('https://www.example.com/')
await asyncio.sleep(3)
app = FastAPI()
#app.on_event("startup")
def startup_function():
app.state.shared_object = MySharedObject()
asyncio.create_task(timed_checker(app.state.shared_object))
#app.get("/")
async def root():
return {"hello": "world"}
#app.get("/count")
async def get_count():
return app.state.shared_object.count
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

How restart dicord bot after client.close(). Discord.py

I make disocord bot with api in flask. Now it just needs to send message to channel. But if I start bot with flask all flask code stops. I tried to use client.close and I had exception RuntimeError: Event loop is closed. I tried to use client.clean in different places. But I still had this exception.
Now my code looks so :
from flask import Flask, request
import json
import discord
global data
app = Flask(__name__)
client = discord.Client()
CHANNEL = <id>
#client.event
async def on_ready():
global data
channel = client.get_channel(id=CHANNEL)
await channel.send(f"{data['products'][0]['custom_fields'][0]['discord']}\nะขะพะฒะฐั€ : {data['products'][0]['name']}")
await client.close()
client.clear()
#app.route('/purchase', methods=['POST'])
def purchase():
global data
data = json.loads(request.data)
client.run('token')
return data['products'][0]['custom_fields'][0]['discord']
I can recommend to run it in the thread. I had the same issue when I had not to block the main thread of the game-client (I believe this should work with Flask too). Example can be found here:
import discord
import asyncio
discord_loop = asyncio.get_event_loop()
client = discord.Client(loop=discord_loop, heartbeat_timeout=20, intents=discord.Intents.all())
def init():
try:
asyncio.get_child_watcher()
global discord_loop
discord_loop = asyncio.get_event_loop()
thread = threading.Thread(target=discord_loop.run_forever)
thread.start()
asyncio.run_coroutine_threadsafe(client.connect(reconnect=True), discord_loop)
asyncio.run_coroutine_threadsafe(client.login(token=DiscordSettings.TOKEN, bot=True), discord_loop)
except:
utils.get_and_log_exception_info()
def stop():
asyncio.run_coroutine_threadsafe(client.logout(), discord_loop)
discord_loop.call_soon_threadsafe(discord_loop.stop)
def reconnect():
client.clear()
asyncio.run_coroutine_threadsafe(client.connect(reconnect=True), discord_loop)

Discord.py - Sending a message within a synchronous function

I am currently working on a Python program using Flask and Discord.py. The program will take parameters from an HTTP GET Request, and then the bot will send them in a message to a specific channel in my Discord server. However, I cannot figure out how to send a message every time a new GET Request is sent since it uses an asynchronous function. My current code is
import os
import discord
from discord.ext.commands import Bot
from flask import Flask, request
from threading import Thread
bot = Bot("!")
messageToSend = ""
recievedMessage = ""
app = Flask('')
#app.route('/')
def home():
return messageToSend
#app.route('/login',methods = ['POST', 'GET'])
def login():
global recievedMessage
if request.method == 'GET':
user = request.args.get('nm')
message = request.args.get('msg')
recievedMessage = f"[{user}]: {message}"
print(recievedMessage)
return user
def run ():
app.run(host='0.0.0.0',port=8080)
def keep_alive():
t = Thread(target=run)
t.start()
#bot.command()
async def send():
message = send_message()
channel = bot.get_channel(852280470384410654)
await channel.send(message)
keep_alive()
bot.run(os.getenv('TOKEN'))
I have tried other methods, such as using Asyncio, but none of them have been able to work. Is it possible to do what I am attempting?

Python + Flask + Discord: How to send a message through discord through a flask endpoint?

I'm trying to send a message with discord, activated through a Flask endpoint
I get the following error message when I call http://127.0.0.1:5000/send
RuntimeError: There is no current event loop in thread 'Thread-4'.
I have the following (minimal) code
import discord
from flask import Flask, jsonify
async def my_background_task():
for message in ['a', 'b']:
await client.wait_until_ready()
channel = client.get_channel(CHANNEL_ID)
await channel.send(message)
await client.close()
def sendMessages():
client = discord.Client()
client.loop.create_task(my_background_task())
client.run('SECRET')
app = Flask(__name__)
#app.route('/send')
def send():
sendMessages()
Maybe you should consider using webhooks instead of a bot. Here is a simple example you should implement flask to it.
import requests #dependency
url = "<your url>" #webhook url, from here: https://i.imgur.com/aT3AThK.png
data = {}
#for all params, see https://discordapp.com/developers/docs/resources/webhook#execute-webhook
data["content"] = "message content"
data["username"] = "custom username"
#leave this out if you dont want an embed
data["embeds"] = []
embed = {}
#for all params, see https://discordapp.com/developers/docs/resources/channel#embed-object
embed["description"] = "text in embed"
embed["title"] = "embed title"
data["embeds"].append(embed)
result = requests.post(url, json=data, headers={"Content-Type": "application/json"})
try:
result.raise_for_status()
except requests.exceptions.HTTPError as err:
print(err)
else:
print("Payload delivered successfully, code {}.".format(result.status_code))
#result: https://i.imgur.com/DRqXQzA.png
Sometimes using webhooks cannot provide the required functionality.
If it is possible for you to switch from flask to quart, you can use the direct support for async methods to gain controll over the event loop as you intented to do in your given example.
The following snippet starts the discord bot within the same event loop as the quart server.
#app.before_serving
async def before_serving():
loop = asyncio.get_event_loop()
await client.login(TOKEN) # this could be done outside of this method
# do not use client.run
loop.create_task(client.connect())
A complete minimal working example would look like follows
import discord
import asyncio
from quart import Quart
app = Quart(__name__)
client = discord.Client()
#app.before_serving
async def before_serving():
loop = asyncio.get_event_loop()
await client.login(TOKEN)
loop.create_task(client.connect())
#app.route("/send", methods=["GET"])
async def send_message():
# wait_until_ready and check for valid connection is missing here
channel = client.get_channel(CH_ID)
await channel.send('XYZ')
return 'OK', 200
app.run()
The connect of the client could be triggered by the /send method itself, however it is important to not create a new task at every request.

Categories