diff --git a/README.md b/README.md index a342cb1..e73d30d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This project is built on: ## Installation ```bash -pip install pycord-rest-bot +pip install pycord-reactive-bot ``` ## Quick Start @@ -40,6 +40,25 @@ if __name__ == "__main__": ) ``` +For more examples, check out the [examples directory](/examples) which includes: +- Basic slash command setup +- Button interactions +- Modal forms +- Production deployment configurations + +## How It Works + +Under the hood, Pycord REST creates an HTTP server using FastAPI and Uvicorn that: + +1. Listens for incoming Discord interaction requests on your specified endpoint +2. Verifies the request signature using your application's public key +3. Routes the interaction to the appropriate command handler +4. Returns the response back to Discord + +Unlike traditional Discord bots that maintain a persistent WebSocket connection to Discord's gateway, HTTP-based bots: +- Only wake up when an interaction is received +- Don't receive real-time events from Discord + ## Usage ### Setting up your bot on Discord @@ -94,15 +113,25 @@ app.run( token="YOUR_BOT_TOKEN", public_key="YOUR_PUBLIC_KEY", uvicorn_options={ - "host": "0.0.0.0", - "port": 8000, - "log_level": "info", + "host": "0.0.0.0", # Listen on all network interfaces + "port": 8000, # Port to listen on + "log_level": "info", # Uvicorn logging level # Any valid uvicorn server options }, health=True # Enable /health endpoint ) ``` +### Server Configuration + +For Discord to reach your bot, you need a publicly accessible HTTPS URL. Options include: +- Using a VPS with a domain and SSL certificate +- Deploying to a cloud service like Heroku, Railway, or Fly.io + +### Health Check + +By default, Pycord REST includes a `/health` endpoint that returns a 200 status code. This endpoint is useful for monitoring services like UptimeRobot or health checks. + ## Advanced Usage ### Adding Custom FastAPI Routes @@ -115,6 +144,24 @@ async def custom_endpoint(request: Request): return {"message": "This is a custom endpoint"} ``` +## Development Workflow + +For faster development and testing, you can use tunneling tools to expose your local development server: + +- **ngrok** - Creates a secure tunnel to your localhost + ```bash + # Install ngrok + npm install -g ngrok + + # Expose your local server + ngrok http 8000 + ``` + +- **Cloudflare Tunnel** - Provides a secure connection to your local server +- **localtunnel** - Simple tunnel service for exposing local endpoints + +These tools provide temporary URLs that you can use in the Discord Developer Portal during development, allowing you to test changes quickly without deploying to production. + ## Contributing Contributions are welcome! This project is in early development, so there might be bugs or unexpected behaviors. diff --git a/examples/basic_bot.py b/examples/basic_bot.py new file mode 100644 index 0000000..5bfa438 --- /dev/null +++ b/examples/basic_bot.py @@ -0,0 +1,46 @@ +"""Basic Discord bot example using Pycord REST. + +This is a minimal example showing how to create slash commands. +""" + +import os +from pydoc import describe + +import discord +from dotenv import load_dotenv + +from pycord_rest import App + +# Load environment variables from .env file +load_dotenv() + +app = App() + + +# Simple ping command +@app.slash_command(name="ping", description="Responds with pong!") +async def ping(ctx: discord.ApplicationContext) -> None: + await ctx.respond("Pong!") + + +# Command with parameters +@app.slash_command(name="greet", description="Greets a user") +@discord.option("name", input_type=str, description="The name of the user to greet", required=False) +async def greet(ctx: discord.ApplicationContext, name: str | None = None) -> None: + if name: + await ctx.respond(f"Hello, {name}!") + else: + await ctx.respond(f"Hello, {ctx.author.display_name}!") + + +# Run the app +if __name__ == "__main__": + app.run( + token=os.environ["DISCORD_TOKEN"], + public_key=os.environ["DISCORD_PUBLIC_KEY"], + uvicorn_options={ + "host": "0.0.0.0", # noqa: S104 + "port": 8000, + "log_level": "info", + }, + ) diff --git a/examples/button_example.py b/examples/button_example.py new file mode 100644 index 0000000..741d64a --- /dev/null +++ b/examples/button_example.py @@ -0,0 +1,52 @@ +"""Example demonstrating how to use buttons with Pycord REST.""" + +import os +from typing import Any + +import discord +from dotenv import load_dotenv + +from pycord_rest import App + +# Load environment variables from .env file +load_dotenv() + +app = App() + + +class MyView(discord.ui.View): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.add_item( + discord.ui.Button( + style=discord.ButtonStyle.link, label="GitHub", url="https://github.com/Paillat-dev/pycord-rest" + ) + ) + + @discord.ui.button(label="Green", style=discord.ButtonStyle.success) + async def green_button(self, button: "discord.ui.Button[MyView]", interaction: discord.Interaction) -> None: + await interaction.respond("You clicked the green button!", ephemeral=True) + + @discord.ui.button(label="Red", style=discord.ButtonStyle.danger) + async def red_button(self, button: "discord.ui.Button[MyView]", interaction: discord.Interaction) -> None: + await interaction.respond("You clicked the red button!", ephemeral=True) + + +# Create a slash command that shows buttons +@app.slash_command(name="buttons", description="Shows interactive buttons") +async def buttons(ctx: discord.ApplicationContext) -> None: + # Create a view with buttons + view = MyView() + await ctx.respond("Choose a button:", view=view) + + +if __name__ == "__main__": + app.run( + token=os.environ["DISCORD_TOKEN"], + public_key=os.environ["DISCORD_PUBLIC_KEY"], + uvicorn_options={ + "host": "0.0.0.0", # noqa: S104 + "port": 8000, + "log_level": "info", + }, + ) diff --git a/examples/modal_example.py b/examples/modal_example.py new file mode 100644 index 0000000..aaa4dcf --- /dev/null +++ b/examples/modal_example.py @@ -0,0 +1,60 @@ +"""Example showing how to work with modals in Pycord REST.""" + +import asyncio +import os +from typing import Any + +import discord +from dotenv import load_dotenv + +from pycord_rest import App + +# Load environment variables from .env file +load_dotenv() + +app = App() + + +class MyModal(discord.ui.Modal): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__( + discord.ui.InputText( + label="Name", placeholder="Enter your name", style=discord.InputTextStyle.short, custom_id="name_input" + ), + discord.ui.InputText( + label="Feedback", + placeholder="Please provide your feedback here...", + style=discord.InputTextStyle.paragraph, + custom_id="feedback_input", + ), + *args, + **kwargs, + ) + + async def callback(self, interaction: discord.Interaction) -> None: + name = self.children[0].value + + await interaction.respond( + f"Thank you for your feedback, {name}! Your submission has been received.", ephemeral=True + ) + + +# Command that shows a form modal +@app.slash_command(name="feedback", description="Submit feedback through a form") +async def feedback(ctx: discord.ApplicationContext) -> None: + # Create a modal + modal = MyModal(title="Feedback Form") + await ctx.send_modal(modal) + await ctx.respond("Opening feedback form...", ephemeral=True) + + +if __name__ == "__main__": + app.run( + token=os.environ["DISCORD_TOKEN"], + public_key=os.environ["DISCORD_PUBLIC_KEY"], + uvicorn_options={ + "host": "0.0.0.0", # noqa: S104 + "port": 8000, + "log_level": "info", + }, + ) diff --git a/pyproject.toml b/pyproject.toml index 41af92e..496906b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12" ] keywords = ["discord", "bot", "rest", "pycord"] @@ -50,6 +49,7 @@ source_archive = "https://github.com/Paillat-dev/pycord-rest/archive/{commit_has name = "pycord-rest-bot" [tool.hatch.build] +packages = ["src/pycord_rest"] exclude = [ ".copywrite.hcl", ".github", @@ -62,8 +62,13 @@ include = [ [tool.pyright] pythonVersion = "3.12" +typeCheckingMode = "all" +reportUnusedCallResult = false reportAny = false -executionEnvironments = [{ root = "src/pycord_rest/_version.py", reportDeprecated = false }] +executionEnvironments = [ + { root = "src/pycord_rest/_version.py", reportDeprecated = false }, + { root = "examples", reportExplicitAny = false, reportUnknownMemberType = false, reportUnusedParameter = false, reportImplicitOverride = false } +] [tool.ruff] target-version = "py312" @@ -83,6 +88,7 @@ exclude = [ [tool.ruff.lint] select = ["ALL"] +per-file-ignores = { "examples/**/*" = ["INP001", "ARG002"] } extend-ignore = [ "N999", "D104",