Skip to content

Commit

Permalink
Merge branch 'next'
Browse files Browse the repository at this point in the history
  • Loading branch information
davidetacchini committed Sep 8, 2023
2 parents de212ed + 7f379aa commit 6e876aa
Show file tree
Hide file tree
Showing 28 changed files with 990 additions and 1,136 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Set up Python '3.10'
- name: Set up Python '3.11'
uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: '3.11'

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019-2021 Davide Tacchini
Copyright (c) 2019-2023 Davide Tacchini

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
50 changes: 2 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,14 @@
<a href="https://pypi.org/project/discord.py/" traget="_blank">
<img src="https://img.shields.io/pypi/v/discord.py" alt="PyPI" />
</a>
<a href="https://ow-api.com/docs/" traget="_blank">
<img src="https://img.shields.io/badge/API-ow--api-orange" alt="API" />
<a href="https://overfast-api.tekrop.fr/" traget="_blank">
<img src="https://img.shields.io/badge/API-OverFast-orange" alt="API" />
</a>
<a href="https://discord.com/invite/8g3jnxv" traget="_blank">
<img src="https://discord.com/api/guilds/550685823784321035/embed.png" alt="Discord Server" />
</a>
<a href="https://top.gg/bot/547546531666984961" traget="_blank">
<img src="https://top.gg/api/widget/servers/547546531666984961.svg?noavatar=true" alt="Server Count" />
</a>
</p>

Self Hosting
------
I would appreciate if you don't host my bot.
However, if you want to test it out, the installation steps are as follows:

**Note**: It is recommended to run the latest stable version of both [Python](https://www.python.org/doc/versions/) and [PostgreSQL](https://www.postgresql.org/docs/release/).

1. **Set up the PostgreSQL database by running the `psql` command**
```sql
CREATE DATABASE overbot;
CREATE USER davide WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE overbot TO davide;
```

2. **Set up the bot and run it**

**Linux**
```bash
git clone https://github.com/davidetacchini/overbot.git
cd overbot
python3 -m venv env
source env/bin/activate
pip install --upgrade pip setuptools # fix build failing
pip install -r requirements.txt
./scripts/init.sh
python3 bot.py
```

**MacOS and Windows**
1. Clone the repository
```bash
git clone https://github.com/davidetacchini/overbot.git
```
2. Load `schema.sql` to the *overbot* database
3. Setup a virtual environment
4. Install the dependencies
```bash
pip install -r requirements.txt
```
5. Rename `config.example.py` to `config.py`
6. Edit the `config.py` file
7. Use `python3 bot.py` to run the bot

Contributing
------
OverBot uses [black](https://pypi.org/project/black/), [isort](https://pypi.org/project/isort/) and [flake8](https://pypi.org/project/flake8/) as code style.
Expand Down
65 changes: 54 additions & 11 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@
from aiohttp import ClientSession
from discord.ext import commands

import config # pyright: reportMissingImports=false
import config

from utils import emojis
from classes.ui import PromptView
from utils.time import human_timedelta
from utils.scrape import get_overwatch_heroes
from classes.paginator import Paginator
from utils.error_handler import error_handler
from classes.command_tree import OverBotCommandTree

if sys.platform == "linux":
import uvloop
Expand All @@ -27,7 +26,7 @@

log = logging.getLogger("overbot")

__version__ = "5.4.0"
__version__ = "6.0.0"


class OverBot(commands.AutoShardedBot):
Expand All @@ -37,15 +36,20 @@ class OverBot(commands.AutoShardedBot):
app_info: discord.AppInfo

def __init__(self, **kwargs: Any) -> None:
super().__init__(command_prefix=config.default_prefix, **kwargs)
super().__init__(
command_prefix=config.default_prefix, tree_cls=OverBotCommandTree, **kwargs
)
self.config = config
self.sloc: int = 0

# caching
self.premiums: set[int] = set()
self.embed_colors: dict[int, int] = {}
self.heroes: dict[str, dict[str, str]] = {}
self.heroes: dict[str, dict[Any, Any]] = {}
self.maps: dict[str, dict[Any, Any]] = {}
self.gamemodes: dict[str, dict[Any, Any]] = {}

self.BASE_URL: str = config.base_url
self.TEST_GUILD: discord.Object = discord.Object(config.test_guild_id)

@property
Expand Down Expand Up @@ -156,9 +160,6 @@ async def _cache_premiums(self) -> None:
ids = await self.pool.fetch(query)
self.premiums = {i["id"] for i in ids}

async def _cache_heroes(self) -> None:
self.heroes = await get_overwatch_heroes()

async def _cache_embed_colors(self) -> None:
embed_colors = {}
query = "SELECT id, embed_color FROM member WHERE embed_color IS NOT NULL;"
Expand All @@ -167,6 +168,48 @@ async def _cache_embed_colors(self) -> None:
embed_colors[member_id] = color
self.embed_colors = embed_colors

async def _cache_heroes(self) -> None:
try:
data = await self.session.get(f"{self.BASE_URL}/heroes")
except Exception:
log.exception("Cannot get heroes. Aborting...")
await self.close()
else:
data = await data.json()
heroes = {}
for hero in data:
heroes[hero.pop("key")] = hero
self.heroes = heroes
log.info("Heroes successfully cached.")

async def _cache_maps(self) -> None:
try:
data = await self.session.get(f"{self.BASE_URL}/maps")
except Exception:
log.exception("Cannot get maps. Aborting...")
await self.close()
else:
data = await data.json()
maps = {}
for map_ in data:
maps[map_.get("name")] = map_
self.maps = maps
log.info("Maps successfully cached.")

async def _cache_gamemodes(self) -> None:
try:
data = await self.session.get(f"{self.BASE_URL}/gamemodes")
except Exception:
log.exception("Cannot get gamemodes. Aborting...")
await self.close()
else:
data = await data.json()
gamemodes = {}
for gamemode in data:
gamemodes[gamemode.pop("key")] = gamemode
self.gamemodes = gamemodes
log.info("Gamemodes successfully cached.")

async def setup_hook(self) -> None:
self.session = ClientSession()
self.pool = await asyncpg.create_pool(**config.database, max_size=20, command_timeout=60.0)
Expand All @@ -179,6 +222,8 @@ async def setup_hook(self) -> None:
await self._cache_premiums()
await self._cache_embed_colors()
await self._cache_heroes()
await self._cache_maps()
await self._cache_gamemodes()

for extension in os.listdir("cogs"):
if extension.endswith(".py"):
Expand All @@ -189,8 +234,6 @@ async def setup_hook(self) -> None:
else:
log.info(f"Extension {extension} successfully loaded.")

self.tree.on_error = error_handler

if self.debug:
self.tree.copy_global_to(guild=self.TEST_GUILD)
await self.tree.sync(guild=self.TEST_GUILD)
Expand Down
136 changes: 136 additions & 0 deletions classes/command_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import logging
import traceback

import discord

from asyncpg import DataError
from discord import app_commands

from classes.exceptions import (
NoChoice,
NotOwner,
NotPremium,
OverBotException,
ProfileNotLinked,
ProfileLimitReached,
)

log = logging.getLogger("overbot")


class OverBotCommandTree(app_commands.CommandTree):
async def on_error(
self, interaction: discord.Interaction, error: app_commands.AppCommandError
) -> None:
bot = interaction.client
command = interaction.command
original_error = getattr(error, "original", error)

if command is not None:
if command._has_any_error_handlers():
return

if isinstance(original_error, discord.NotFound):
return

async def send(payload: str | discord.Embed, ephemeral: bool = True) -> None:
if isinstance(payload, str):
kwargs = {"content": payload}
elif isinstance(payload, discord.Embed):
kwargs = {"embed": payload}

if interaction.response.is_done():
await interaction.followup.send(**kwargs, ephemeral=ephemeral)
else:
await interaction.response.send_message(**kwargs, ephemeral=ephemeral)

if isinstance(error, app_commands.CommandNotFound):
return

elif isinstance(error, app_commands.TransformerError):
await send(str(error))

elif isinstance(error, app_commands.CheckFailure):
if isinstance(error, ProfileNotLinked):
if error.is_author:
await send("You haven't linked a profile yet. Use /profile link to start.")
else:
await send("This user did not link a profile yet.")

elif isinstance(error, ProfileLimitReached):
if error.limit == 5:
premium = bot.config.premium
embed = discord.Embed(color=discord.Color.red())
embed.description = (
"Maximum limit of profiles reached.\n"
f"[Upgrade to Premium]({premium}) to be able to link up to 25 profiles."
)
await send(embed)
else:
await send("Maximum limit of profiles reached.")

elif isinstance(error, NotPremium):
premium = bot.config.premium
embed = discord.Embed(color=discord.Color.red())
embed.description = (
"This command requires a Premium membership.\n"
f"[Click here]({premium}) to have a look at the Premium plans."
)
await send(embed)

elif isinstance(error, NotOwner):
await send("You are not allowed to run this command.")

elif isinstance(error, app_commands.NoPrivateMessage):
await send("This command cannot be used in direct messages.")

elif isinstance(error, app_commands.MissingPermissions):
perms = ", ".join(map(lambda p: f"`{p}`", error.missing_permissions))
await send(f"You don't have enough permissions to run this command: {perms}")

elif isinstance(error, app_commands.BotMissingPermissions):
perms = ", ".join(map(lambda p: f"`{p}`", error.missing_permissions))
await send(f"I don't have enough permissions to run this command: {perms}")

elif isinstance(error, app_commands.CommandOnCooldown):
command = interaction.command.qualified_name
seconds = round(error.retry_after, 2)
await send(f"You can't use `{command}` command for `{seconds}s`.")

elif isinstance(error, app_commands.CommandInvokeError):
original = getattr(error, "original", error)
if isinstance(original, DataError):
await send("The argument you entered cannot be handled.")
elif isinstance(original, NoChoice):
pass
elif isinstance(original, OverBotException):
await send(str(original))
else:
embed = discord.Embed(color=discord.Color.red())
embed.set_author(
name=str(interaction.user), icon_url=interaction.user.display_avatar
)
embed.add_field(name="Command", value=interaction.command.qualified_name)
if interaction.guild:
guild = f"{str(interaction.guild)} ({interaction.guild_id})"
embed.add_field(name="Guild", value=guild, inline=False)
try:
exc = "".join(
traceback.format_exception(
type(original),
original,
original.__traceback__,
chain=False,
)
)
except AttributeError:
exc = f"{type(original)}\n{original}"
embed.description = f"```py\n{exc}\n```"
embed.timestamp = interaction.created_at
if not bot.debug:
await bot.webhook.send(embed=embed)
else:
log.exception(original.__traceback__)
await send(
"This command ran into an error. The incident has been reported and will be fixed as soon as possible."
)
Loading

0 comments on commit 6e876aa

Please sign in to comment.