Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skullboard #14

Merged
merged 23 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ef98a49
feat: Add skullboard that updates based on skull count
phoenixpereira Jun 27, 2024
fdb890d
refactor Move Adelaide time function to seperate file and combine sha…
phoenixpereira Jun 27, 2024
451a433
feat: Handles reacts to messages before server started
phoenixpereira Jun 27, 2024
764aa12
chore: Replace utcnow with now
phoenixpereira Jun 27, 2024
548a16d
refactor: Move required reactions count and data file name to .env
phoenixpereira Jun 27, 2024
4ea61a4
refactor: Update skullboard message handling logic
phoenixpereira Jun 27, 2024
1666f50
fix: Use message time instead of current time
phoenixpereira Jun 27, 2024
70d77cc
refactor: Move skullboard functions into a class
phoenixpereira Jul 7, 2024
5e29710
feat: Add user nickname and avatar to skullboard messages
phoenixpereira Jul 7, 2024
ae9ab6c
fix: Ignore reactions to own messages
phoenixpereira Jul 7, 2024
e3a7b9a
style: Run Black
phoenixpereira Jul 7, 2024
8e7ff02
refactor: Make skullboard function without data file
phoenixpereira Jul 8, 2024
ca74746
chore: Add colour to skullboard embed
phoenixpereira Jul 8, 2024
bd1ea67
chore: Update production workflow to use environment variables for sk…
phoenixpereira Jul 8, 2024
a3c5766
feature: Support images, stickers, and gifs
phoenixpereira Jul 10, 2024
145e96d
Merge branch 'main' into skullboard
phoenixpereira Jul 22, 2024
46a5a6d
fix: Update pyproject.toml
phoenixpereira Jul 22, 2024
faca79e
style: Run Black
phoenixpereira Jul 22, 2024
ac631cc
chore: Clean up code
phoenixpereira Jul 22, 2024
647bdbb
feat: Handle video attachments
phoenixpereira Jul 22, 2024
755dae3
chore: Remove unneeded environment variables in production workflow
phoenixpereira Jul 23, 2024
bb267aa
refactor: Improve code organisation and readability based on feedback
phoenixpereira Jul 23, 2024
4329460
style: Seperate imports by type
phoenixpereira Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .example.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
GUILD_ID=GUILD_ID
BOT_TOKEN="BOT_TOKEN"
SKULLBOARD_CHANNEL_ID=SKULLBOARD_CHANNEL_ID
REQUIRED_REACTIONS=5
4 changes: 0 additions & 4 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,12 @@ jobs:
KEY: ${{ secrets.SSH_EC2_KEY }}
HOSTNAME: ${{ secrets.SSH_EC2_HOSTNAME }}
USER: ${{ secrets.SSH_EC2_USER }}
GUILD_ID: ${{ secrets.GUILD_ID }}
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
run: |
echo "$KEY" > private_key && chmod 600 private_key
ssh -v -o StrictHostKeyChecking=no -i private_key ${USER}@${HOSTNAME} '
cd ~/duckbot
aws s3 cp s3://${{ secrets.AWS_S3_BUCKET }}/duckbot/duckbot.tar.gz .
aws s3 cp s3://${{ secrets.AWS_S3_BUCKET }}/duckbot/docker-compose.yml .
echo GUILD_ID=${{ secrets.GUILD_ID }} > .env
echo BOT_TOKEN=${{ secrets.BOT_TOKEN }} >> .env
docker load -i duckbot.tar.gz
docker compose up -d
'
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

151 changes: 151 additions & 0 deletions src/commands/skullboard.py
phoenixpereira marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import os
import requests
import re

from discord import Embed, Client
from dotenv import load_dotenv

from constants.colours import LIGHT_GREY


class SkullboardManager:
def __init__(self, client: Client):
"""Initialise SkullboardManager"""
load_dotenv() # Load environment variables from .env file
self.client = client
self.required_reactions = int(os.getenv("REQUIRED_REACTIONS"))

async def get_reaction_count(self, message, emoji):
"""Get count of a specific emoji reaction on a message"""
return next(
(
reaction.count
for reaction in message.reactions
if reaction.emoji == emoji
),
phoenixpereira marked this conversation as resolved.
Show resolved Hide resolved
0,
)

async def handle_skullboard(self, message, skullboard_channel_id):
"""Handle reactions and update/delete skullboard messages"""
skullboard_channel = self.client.get_channel(skullboard_channel_id)
if not skullboard_channel:
return

emoji = "💀"
current_count = await self.get_reaction_count(message, emoji)

await self.update_or_send_skullboard_message(
skullboard_channel, message, current_count, emoji
)

async def update_or_send_skullboard_message(
self, channel, message, current_count, emoji
):
"""Update or send skullboard message"""
skullboard_message_id = None
message_jump_url = message.jump_url

async for skullboard_message in channel.history(limit=100):
if message_jump_url in skullboard_message.content:
skullboard_message_id = skullboard_message.id
break

if current_count >= self.required_reactions:
if skullboard_message_id:
await self.edit_or_send_skullboard_message(
channel,
message,
current_count,
emoji,
send=False,
skullboard_message_id=skullboard_message_id,
)
else:
await self.edit_or_send_skullboard_message(
channel, message, current_count, emoji, send=True
)
elif skullboard_message_id:
skullboard_message = await channel.fetch_message(skullboard_message_id)
await skullboard_message.delete()

@staticmethod
async def get_gif_url(view_url):
"""Get URL of GIF from a Tenor view URL"""
# Get the page content
page_content = requests.get(view_url).text

# Regex to find the URL on the media.tenor.com domain that ends with .gif
regex = r"(?i)\b((https?://media1[.]tenor[.]com/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))[.]gif)"

# Find and return the first match
match = re.findall(regex, page_content)

return match[0][0] if match else None

async def edit_or_send_skullboard_message(
self,
channel,
message,
current_count,
emoji,
send=False,
skullboard_message_id=None,
):
"""Edit or send a skullboard message"""
# Fetch user's nickname and avatar url
guild = self.client.get_guild(message.guild.id)
member = guild.get_member(message.author.id)
user_nickname = member.nick if member.nick else message.author.name
user_avatar_url = message.author.avatar.url

# Constructing the message content
message_jump_url = message.jump_url
message_content = f"{emoji} {current_count} | {message_jump_url}"

# Constructing the embed
embed = Embed(
description=f"{message.content}\n\n",
timestamp=message.created_at,
colour=LIGHT_GREY,
)

if message.content.startswith("https://tenor.com/view/"):
# Constructing the embed
embed = Embed(
timestamp=message.created_at,
colour=LIGHT_GREY,
)

# Find the URL of the gif
gif_url = await self.get_gif_url(message.content)

if gif_url:
embed.set_image(url=gif_url)

# Set user nickname and thumbnail
embed.set_author(name=user_nickname, icon_url=user_avatar_url)

# Add images, stickers, and attachments
if message.stickers:
# Replace the pattern with just the format type
format_type = str(message.stickers[0].format).split(".", maxsplit=1)[-1]

sticker_id = message.stickers[0].id
sticker_url = f"https://media.discordapp.net/stickers/{
sticker_id}.{format_type}"
embed.set_image(url=sticker_url)

if message.attachments:
attachment = message.attachments[0]
if attachment.content_type.startswith("video"):
embed.add_field(name="", value=attachment.url)
else:
embed.set_image(url=attachment.url)

# Determine if sending or editing the message
if send:
await channel.send(message_content, embed=embed)
else:
skullboard_message = await channel.fetch_message(skullboard_message_id)
await skullboard_message.edit(content=message_content, embed=embed)
3 changes: 3 additions & 0 deletions src/constants/colours.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from discord import Color

LIGHT_GREY = Color.from_rgb(204, 214, 221)
47 changes: 46 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import os
import importlib
import pkgutil
from discord import Intents, app_commands, Object, Interaction, Embed, Message, Color

from discord import (
Intents,
app_commands,
Object,
Interaction,
Embed,
Color,
Message,
RawReactionActionEvent,
)
from discord.ext import commands
from dotenv import load_dotenv

from commands import skullboard

# Load environment variables from .env file
load_dotenv()

# Retrieve guild ID and bot token from environment variables
GUILD_ID = int(os.environ["GUILD_ID"])
BOT_TOKEN = os.environ["BOT_TOKEN"]
SKULLBOARD_CHANNEL_ID = int(os.environ["SKULLBOARD_CHANNEL_ID"])

# Load the permissions the bot has been granted in the previous configuration
intents = Intents.default()
intents.guilds = True
intents.messages = True
intents.message_content = True
intents.reactions = True
intents.members = True


class DuckBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix="", intents=intents)
self.synced = False # Make sure that the command tree will be synced only once
self.skullboard_manager = skullboard.SkullboardManager(
self
) # Initialise SkullboardManager

async def setup_hook(self):
# Dynamically load all command groups from the commands directory
Expand All @@ -38,6 +58,31 @@ async def setup_hook(self):
async def on_ready(self):
print(f"Say hi to {self.user}!")

# Override on_message method with correct parameters
async def on_message(self, message):
pass

# Register the reaction handling
async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
if payload.emoji.name == "💀":
channel = self.get_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
# Ignore reactions to own messages
if message.author.id != self.user.id:
await self.skullboard_manager.handle_skullboard(
message, SKULLBOARD_CHANNEL_ID
)

async def on_raw_reaction_remove(self, payload: RawReactionActionEvent):
if payload.emoji.name == "💀":
channel = self.get_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
# Ignore reactions to own messages
if message.author.id != self.user.id:
await self.skullboard_manager.handle_skullboard(
message, SKULLBOARD_CHANNEL_ID
)


client = DuckBot()

Expand Down