Skip to content

Commit

Permalink
Merge branch 'dev' into adventofcode
Browse files Browse the repository at this point in the history
  • Loading branch information
FirePlank authored Nov 14, 2023
2 parents 9a3e91e + d89ef8f commit e7c1be5
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 21 deletions.
70 changes: 70 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Contributing

## Setup Discord Application

Refer to the official discord.py [documentation](https://discordpy.readthedocs.io/en/stable/discord.html) to create a bot.

Also, please make sure that the scope application.commands is enabled.

Make sure to enable intents as well—check the official guide for detailed instructions [here](https://discordpy.readthedocs.io/en/latest/intents.html).

<img width="1313" alt="Screenshot 2023-11-14 at 12 51 00 PM" src="https://github.com/sarzz2/Discord-Bot/assets/45039724/97fd0d7f-8e6b-4d7b-b4f9-008a07dfb26b">



## Project Setup & Installation

1. Fork the repository to your own profile.
2. Set up a local PostgreSQL database or use [Docker](#Docker-Setup).

```postgresql://username:password@localhost/db_name```

Replace username, password, db_name with appropriate values.

3. To install packages, run:

```bash
pip install poetry
poetry install
```
4. Once all the packages are installed, run migrations using:

```python cli.py migrate up```

5. Create a .env file and copy the contents of example.env, setting up the environment variables.

6. Once the above steps are done, run the bot using the command:

```bash
python cli.py
```

7. Feel free to join the server in case of any issues.

## Docker-Setup

1. Ensure you have Docker installed and set up on your system. Refer to the [Docker's official guide](https://docs.docker.com/get-started/overview/) if needed.
2. To run a PostgreSQL instance, execute:

```bash
docker compose up -d postgres
```
The `-d` flag runs the instance in detached mode.

3. Run migrations.

```python cli.py migrate up```
# Guidelines

Please adhere to the following guidelines when contributing:

- Follow the coding style and conventions used in the project.
- Write clear and concise commit messages.
- Test your changes thoroughly before submitting a pull request.
- Be respectful and constructive in all interactions with other contributors.

## Code Quality and Testing

- Ensure your code follows best practices and is well-documented.
- Write unit tests for new features and ensure existing tests pass.
- Perform code reviews and address feedback from other contributors.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
# Discord-Bot
#### Discord bot for Tech With Tim's discord server.
[![Invitation link](https://discord.com/api/guilds/501090983539245061/widget.png?style=banner3)](https://discord.gg/twt)

# Contributing

## Contributing
* Fork the repository to your own profile, edit the code there.
* When you are done with a feature open a pull request.
* If you have any questions contact us in our discord server.

[![Invitation link](https://discord.com/api/guilds/501090983539245061/widget.png?style=banner3)](https://discord.gg/twt)
If you're interested in contributing, please review the [Contributing](CONTRIBUTING.md) section. For specific tasks and opportunities, explore our list of open [issues](https://github.com/SylteA/Discord-Bot/issues).
8 changes: 6 additions & 2 deletions bot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@ def val_func(cls, v):


class Notification(BaseModel):
api_key: str # Youtube Data AP - API Key: https://developers.google.com/youtub/docs
channel_id: int
role_id: int
webhook: str


class Postgres(BaseModel):
Expand Down Expand Up @@ -99,6 +97,11 @@ class ErrorHandling(BaseModel):
webhook_url: str


class CustomRoles(BaseModel):
log_channel_id: int
divider_role_id: int


class Settings(BaseSettings):
aoc: AoC
bot: Bot
Expand All @@ -113,6 +116,7 @@ class Settings(BaseSettings):
timathon: Timathon
hastebin: Hastebin
errors: ErrorHandling
custom_roles: CustomRoles

class Config:
env_file = ".env"
Expand Down
10 changes: 8 additions & 2 deletions bot/core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import json
import logging
import os
import traceback
Expand Down Expand Up @@ -113,8 +114,12 @@ async def on_app_command_error(self, interaction: "InteractionType", error: app_

async def publish_error(self, interaction: "InteractionType", error: app_commands.AppCommandError) -> None:
"""Publishes the error to our error webhook."""
content = "\n".join(traceback.format_exception(type(error), error, error.__traceback__))
header = f"Ignored exception in command **{interaction.command.qualified_name}**"
content = "".join(traceback.format_exception(type(error), error, error.__traceback__))
header = (
f"Ignored exception in command **{interaction.command.qualified_name}** Invoked by **{interaction.user}**"
f"in channel **{interaction.channel.name}**"
)
invoked_details_document = await paste.create(str(json.dumps(interaction.data, indent=2)))

def wrap(code: str) -> str:
code = code.replace("`", "\u200b`")
Expand All @@ -129,6 +134,7 @@ def wrap(code: str) -> str:
embed = discord.Embed(
title=header, description=content, color=discord.Color.red(), timestamp=discord.utils.utcnow()
)
embed.add_field(name="Command Details: ", value=invoked_details_document.url, inline=True)
await self.error_webhook.send(embed=embed)

@tasks.loop(hours=24)
Expand Down
9 changes: 9 additions & 0 deletions bot/extensions/custom_roles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from bot.core import DiscordBot

from .commands import CustomRoles
from .events import CustomRoleEvents


async def setup(bot: DiscordBot) -> None:
await bot.add_cog(CustomRoles(bot=bot))
await bot.add_cog(CustomRoleEvents(bot=bot))
110 changes: 110 additions & 0 deletions bot/extensions/custom_roles/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import annotations

import discord
from discord import app_commands, utils
from discord.ext import commands

from bot import core
from bot.config import settings
from bot.models import CustomRole


class CustomRoles(commands.Cog):
def __init__(self, bot):
self.bot = bot

self.color_converter = commands.ColorConverter()

@staticmethod
def role_embed(heading: str, role: discord.Role):
embed = discord.Embed(
description=f"**{heading}**",
timestamp=role.created_at,
color=role.color,
)
embed.add_field(name="Name", value=utils.escape_markdown(role.name))
embed.add_field(name="Color", value=str(role.color))
embed.set_footer(text="Created at")
return embed

@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.describe(name="New name", color="New color")
async def myrole(
self, interaction: core.InteractionType, name: app_commands.Range[str, 2, 100] | None, color: str = None
):
"""Manage your custom role"""
if color is not None:
try:
color = await self.color_converter.convert(None, color) # noqa
except commands.BadColourArgument as e:
return await interaction.response.send_message(str(e), ephemeral=True)

query = "SELECT * FROM custom_roles WHERE guild_id = $1 AND user_id = $2"
before = await CustomRole.fetchrow(query, interaction.guild.id, interaction.user.id)

if before is None:
if name is None:
return await interaction.response.send_message("You don't have a custom role yet!", ephemeral=True)

# Create and assign the role to user
role = await interaction.guild.create_role(name=name, colour=color or discord.Color.random())

divider_role = interaction.guild.get_role(settings.custom_roles.divider_role_id)
await role.edit(position=divider_role.position + 1)

record = await CustomRole.ensure_exists(
guild_id=interaction.guild.id,
user_id=interaction.user.id,
role_id=role.id,
name=role.name,
color=role.color.value,
)

self.bot.dispatch("custom_role_create", custom_role=record)
self.bot.dispatch(
"persist_roles",
guild_id=interaction.guild.id,
user_id=interaction.user.id,
role_ids=[role.id],
)

return await interaction.response.send_message(
embed=self.role_embed("**Custom Role has been assigned**", role),
ephemeral=True,
)

role = interaction.guild.get_role(before.role_id)

# Return role information if no parameter is passed
if (name is None or name == before.name) and (color is None or color.value == before.color):
return await interaction.response.send_message(
embed=self.role_embed(
f"Custom Role for {interaction.user.mention}", interaction.guild.get_role(before.role_id)
),
ephemeral=True,
)

await role.edit(
name=name or before.name,
colour=color or discord.Color(int(before.color)),
)

after = await CustomRole.ensure_exists(
guild_id=interaction.guild.id,
user_id=interaction.user.id,
role_id=role.id,
name=role.name,
color=role.color.value,
)

self.bot.dispatch("custom_role_update", before, after)

return await interaction.response.send_message(
embed=self.role_embed("**Custom Role has been updated**", interaction.guild.get_role(before.role_id)),
ephemeral=True,
)


async def setup(bot: commands.Bot):
await bot.add_cog(CustomRoles(bot=bot))
80 changes: 80 additions & 0 deletions bot/extensions/custom_roles/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import datetime
import time

import discord
from discord.ext import commands

from bot import core
from bot.config import settings
from bot.models.custom_roles import CustomRole


class CustomRoleEvents(commands.Cog):
"""Events for Levelling in discord."""

def __init__(self, bot: core.DiscordBot):
self.bot = bot
self.updated_at: dict[int, datetime] = {}

@property
def custom_roles_logs_channel(self) -> discord.TextChannel | None:
return self.bot.guild.get_channel(settings.custom_roles.log_channel_id)

@commands.Cog.listener()
async def on_guild_role_update(self, before: discord.Role, after: discord.Role):
last_update = self.updated_at.get(before.id)

# Ignore events less than 10 seconds since a user updated their role
if last_update is not None:
if time.time() - last_update < 10:
return

query = """
UPDATE custom_roles
SET name = $2, color = $3
WHERE role_id = $1
"""
await CustomRole.execute(query, after.id, after.name, after.color.value)

@commands.Cog.listener()
async def on_custom_role_create(self, custom_role: CustomRole):
"""Logs the creation of new role"""
self.updated_at[custom_role.role_id] = time.time()

user = await self.bot.fetch_user(custom_role.user_id)

embed = discord.Embed(
title="Custom Role Created",
color=discord.Color.brand_green(),
timestamp=datetime.datetime.utcnow(),
)
embed.set_author(name=user.display_name, icon_url=user.display_avatar)
embed.add_field(name="Name", value=custom_role.name)
embed.add_field(name="Color", value="#" + hex(custom_role.color)[2:])
embed.set_thumbnail(url=user.avatar)
embed.set_footer(text=f"user_id: {custom_role.user_id}")

return await self.custom_roles_logs_channel.send(embed=embed)

@commands.Cog.listener()
async def on_custom_role_update(self, before: CustomRole, after: CustomRole):
"""Logs the update of custom role."""
self.updated_at[after.role_id] = time.time()

user = await self.bot.fetch_user(after.user_id)

embed = discord.Embed(
title="Custom Role Updated",
color=discord.Color.brand_green(),
timestamp=datetime.datetime.utcnow(),
)

embed.add_field(name="Old Name", value=before.name)
embed.add_field(name="New Name", value=after.name)
embed.add_field(name="\u200B", value="\u200B")
embed.add_field(name="Old Color", value="#" + hex(before.color)[2:])
embed.add_field(name="New Color", value="#" + hex(after.color)[2:])
embed.set_thumbnail(url=user.avatar)
embed.set_footer(text=f"user_id: {after.user_id}")

return await self.custom_roles_logs_channel.send(embed=embed)
4 changes: 2 additions & 2 deletions bot/extensions/levelling/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async def on_message(self, message):
"""

# TODO: Allow each guild to set custom xp range and boost.
xp = random.randint(5, 25) * self.xp_boost
xp = random.randint(15, 25) * self.xp_boost
after = await LevellingUser.fetchrow(query, message.guild.id, message.author.id, xp)

if after is None:
Expand Down Expand Up @@ -282,7 +282,7 @@ async def rank(self, interaction: core.InteractionType, member: discord.Member =
return await interaction.response.send_message("That user is not ranked yet...", ephemeral=True)

# Fetch the user's avatar as bytes
avatar_bytes = await member.avatar.with_format("png").read()
avatar_bytes = await member.display_avatar.with_format("png").read()

level = utils.get_level_for_xp(record.total_xp)
prev_xp = utils.get_xp_for_level(level)
Expand Down
7 changes: 7 additions & 0 deletions bot/extensions/youtube/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from bot.core import DiscordBot

from .tasks import YoutubeTasks


async def setup(bot: DiscordBot) -> None:
await bot.add_cog(YoutubeTasks(bot=bot))
Loading

0 comments on commit e7c1be5

Please sign in to comment.