goober/assets/cogs/fireboard.py
2025-07-07 23:41:23 +02:00

1923 lines
No EOL
73 KiB
Python

import asyncio
import random
import asqlite
import discord
import discord.ext
import discord.ext.commands
from discord import Color, app_commands
from discord.ext import commands
from discord.ui import View
class Fireboard(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.locked_messages = []
self.disabled = False
# Check if the bot has the fireboard_pool attribute
if not hasattr(bot, 'fireboard_pool') or bot.fireboard_pool is None:
print("Warning: Bot does not have fireboard_pool initialized. Fireboard functionality will be disabled.")
self.disabled = True
return
self.fireboard_pool: asqlite.Pool = bot.fireboard_pool
self.bot.loop.create_task(self.setup())
# SQL Setup
async def setup(self):
async with self.fireboard_pool.acquire() as sql:
if (
await sql.fetchone(
"SELECT name FROM sqlite_master WHERE type='table' AND name='fireMessages';"
)
is None
):
# Fire Messages - messages that are active on the fireboard
await sql.execute(
"CREATE TABLE fireMessages (serverID int, msgID int, boardMsgID int, reactionAmount int)"
)
if (
await sql.fetchone(
"SELECT name FROM sqlite_master WHERE type='table' AND name='fireSettings';"
)
is None
):
# Fire Settings - server properties for fireboard
await sql.execute(
"CREATE TABLE fireSettings (serverID int, reactionAmount int, emoji text, channelID int, ignoreBots int)"
)
if (
await sql.fetchone(
"SELECT name FROM sqlite_master WHERE type='table' AND name='fireChannelBlacklist';"
)
is None
):
# Fire Channel Blacklist - blacklisted channels
await sql.execute(
"CREATE TABLE fireChannelBlacklist (serverID int, channelID int)"
)
if (
await sql.fetchone(
"SELECT name FROM sqlite_master WHERE type='table' AND name='fireRoleBlacklist';"
)
is None
):
# Fire Role Blacklist - blacklisted roles
await sql.execute(
"CREATE TABLE fireRoleBlacklist (serverID int, roleID int)"
)
await sql.commit()
await self.refresh_fire_lists()
# List refresh function
async def refresh_fire_lists(self):
async with self.fireboard_pool.acquire() as sql:
self.fire_messages = await sql.fetchall("SELECT * FROM fireMessages")
self.fire_settings = await sql.fetchall("SELECT * FROM fireSettings")
self.fire_channel_blacklist = await sql.fetchall(
"SELECT * FROM fireChannelBlacklist"
)
self.fire_role_blacklist = await sql.fetchall(
"SELECT * FROM fireRoleBlacklist"
)
# Listen for reactions
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
self.bot: discord.ext.commands.Bot
# Stop if this is a DM
if payload.guild_id is None:
return
queued = False
# Lock system
if payload.message_id in self.locked_messages:
queued = True
while payload.message_id in self.locked_messages:
await asyncio.sleep(0.5)
self.locked_messages.append(payload.message_id)
try:
fetched = False
# Find server config
for server in self.fire_settings:
if server[0] == payload.guild_id:
react_minimum = server[1]
emoji = server[2]
channel_id = server[3]
ignore_bots = True if int(server[4]) == 1 else False
fetched = True
# Stop if server has no config (fireboard isn't enabled)
if not fetched:
return
# Stop if message is by Titanium
if payload.message_author_id == self.bot.user.id:
return
# Stop if emoji doesn't match
if str(payload.emoji) != emoji:
return
# --- Edit board message if it already exists ---
if payload.message_id in [message[1] for message in self.fire_messages]:
message = [
message
for message in self.fire_messages
if message[1] == payload.message_id
][0]
# Only fetch updated reaction count if I have queued or reaction amount is undefined
if queued or message[3] is None:
# Fetch message and channel
try:
msg_channel = await self.bot.fetch_channel(payload.channel_id)
message = await msg_channel.fetch_message(payload.message_id)
except discord.errors.NotFound:
return
# Stop if not enough reactions
for reaction in message.reactions:
if str(reaction.emoji) == emoji:
react_count = reaction.count
break
if react_count < react_minimum:
return
else:
react_count = None
async with self.fireboard_pool.acquire() as sql:
# Set updated react count
if react_count is not None:
await sql.execute(
"UPDATE fireMessages SET reactionAmount = ? WHERE msgID = ?",
(
react_count,
payload.message_id,
),
)
await self.refresh_fire_lists()
else:
await sql.execute(
"UPDATE fireMessages SET reactionAmount = reactionAmount + 1 WHERE msgID = ?",
(payload.message_id,),
)
await self.refresh_fire_lists()
# Get message from message list
message = [
message
for message in self.fire_messages
if message[1] == payload.message_id
][0]
# Get board message
board_channel = await self.bot.fetch_channel(channel_id)
board_message = await board_channel.fetch_message(message[2])
await board_message.edit(
content=f"**{message[3]} {emoji}** | <@{payload.message_author_id}> | <#{payload.channel_id}>",
embeds=board_message.embeds,
)
return
# Stop if message is in a blacklisted channel
if payload.channel_id in [
channel[1] for channel in self.fire_channel_blacklist
]:
return
# Stop if message is by a blacklisted role
guild = await self.bot.fetch_guild(payload.guild_id)
member = await guild.fetch_member(payload.user_id)
if any(
role[1] in [role.id for role in member.roles]
for role in self.fire_role_blacklist
):
return
# Fetch message and channel
try:
msg_channel = await self.bot.fetch_channel(payload.channel_id)
message = await msg_channel.fetch_message(payload.message_id)
except discord.errors.NotFound:
return
# Stop if message is by a bot
if ignore_bots and message.author.bot:
return
# Check if this is a thread
if isinstance(msg_channel, discord.Thread):
# Check if parent channel is NSFW
if msg_channel.parent.nsfw:
return
else:
# Stop if message is in an NSFW channel
if message.channel.nsfw:
return
# Stop if not enough reactions
for reaction in message.reactions:
if str(reaction.emoji) == emoji:
react_count = reaction.count
break
if react_count < react_minimum:
return
# --- Send message to fireboard ---
# Create embed
embed = discord.Embed(description=message.content, color=Color.random())
embed.set_author(
name=message.author.name, icon_url=message.author.display_avatar.url
)
embed.timestamp = message.created_at
# Jump to message button
view = View()
view.add_item(
discord.ui.Button(
label="Jump to Message",
url=message.jump_url,
style=discord.ButtonStyle.url,
)
)
embed_list = [embed]
# Add reply embed
if message.reference:
try:
reply_message = await msg_channel.fetch_message(
message.reference.message_id
)
reply_embed = discord.Embed(
title="Replying To",
description=reply_message.content,
color=Color.random(),
)
reply_embed.set_author(
name=reply_message.author.name,
icon_url=reply_message.author.display_avatar.url,
)
reply_embed.timestamp = reply_message.created_at
embed_list.insert(0, reply_embed)
except discord.errors.NotFound:
pass
# Send message
board_channel = await self.bot.fetch_channel(channel_id)
board_message = await board_channel.send(
content=f"**{react_count} {emoji}** | {message.author.mention} | <#{payload.channel_id}>",
embeds=embed_list,
view=view,
files=[
await attachment.to_file() for attachment in message.attachments
],
)
async with self.fireboard_pool.acquire() as sql:
# Insert message to DB
await sql.execute(
"INSERT INTO fireMessages (serverID, msgID, boardMsgID, reactionAmount) VALUES (?, ?, ?, ?)",
(
payload.guild_id,
payload.message_id,
board_message.id,
react_count,
),
)
await sql.commit()
await self.refresh_fire_lists()
except Exception as e:
raise e
finally:
self.locked_messages.remove(payload.message_id)
# Listen for reaction removal
@commands.Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
self.bot: discord.ext.commands.Bot
queued = False
# Stop if this is a DM
if payload.guild_id is None:
return
# Lock system
if payload.message_id in self.locked_messages:
queued = True
while payload.message_id in self.locked_messages:
await asyncio.sleep(0.5)
self.locked_messages.append(payload.message_id)
try:
fetched = False
# Find server config
for server in self.fire_settings:
if server[0] == payload.guild_id:
react_minimum = server[1]
emoji = server[2]
channel_id = server[3]
ignore_bots = True if int(server[4]) == 1 else False
fetched = True
# Stop if server has no config (fireboard isn't enabled)
if not fetched:
return
# Stop if message is by Titanium
if payload.message_author_id == self.bot.user.id:
return
# Stop if emoji doesn't match
if str(payload.emoji) != emoji:
return
# --- Edit board message if it already exists ---
if payload.message_id in [message[1] for message in self.fire_messages]:
prev_react_count = [
message
for message in self.fire_messages
if message[1] == payload.message_id
][0][3]
# Only fetch updated reaction count if I have queued
if queued or prev_react_count is None:
# Fetch message and channel
try:
msg_channel = await self.bot.fetch_channel(payload.channel_id)
message = await msg_channel.fetch_message(payload.message_id)
except discord.errors.NotFound:
return
# Stop if not enough reactions
for reaction in message.reactions:
if str(reaction.emoji) == emoji:
react_count = reaction.count
break
if react_count < react_minimum:
return
else:
react_count = None
async with self.fireboard_pool.acquire() as sql:
# Set updated react count
if react_count is not None:
await sql.execute(
"UPDATE fireMessages SET reactionAmount = ? WHERE msgID = ?",
(
react_count,
payload.message_id,
),
)
await self.refresh_fire_lists()
else:
await sql.execute(
"UPDATE fireMessages SET reactionAmount = reactionAmount - 1 WHERE msgID = ?",
(payload.message_id,),
)
await self.refresh_fire_lists()
# Get message from message list
message = [
message
for message in self.fire_messages
if message[1] == payload.message_id
][0]
# Get board message
board_channel = await self.bot.fetch_channel(channel_id)
board_message = await board_channel.fetch_message(message[2])
# Remove message if not enough reactions
if message[3] < react_minimum:
await board_message.delete()
async with self.fireboard_pool.acquire() as sql:
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(payload.message_id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
# Workaround for lack of message author ID
content = board_message.content
content = content.replace(
f"{prev_react_count} {emoji}", f"{message[3]} {emoji}"
)
await board_message.edit(content=content)
return
# Stop if message is in a blacklisted channel
if payload.channel_id in [
channel[1] for channel in self.fire_channel_blacklist
]:
return
# Stop if message is by a blacklisted role
guild = await self.bot.fetch_guild(payload.guild_id)
member = await guild.fetch_member(payload.user_id)
if any(
role[1] in [role.id for role in member.roles]
for role in self.fire_role_blacklist
):
return
# Fetch message and channel
try:
msg_channel = await self.bot.fetch_channel(payload.channel_id)
message = await msg_channel.fetch_message(payload.message_id)
except discord.errors.NotFound:
return
# Stop if message is by a bot
if ignore_bots and message.author.bot:
return
# Check if this is a thread
if isinstance(msg_channel, discord.Thread):
# Check if parent channel is NSFW
if msg_channel.parent.nsfw:
return
else:
# Stop if message is in an NSFW channel
if message.channel.nsfw:
return
# Get reaction count
react_count = 0
for reaction in message.reactions:
if str(reaction.emoji) == emoji:
react_count = reaction.count
break
# Stop if not enough reactions
if react_count < react_minimum:
return
# --- Send message to fireboard ---
# Create embed
embed = discord.Embed(description=message.content, color=Color.random())
embed.set_author(
name=message.author.name, icon_url=message.author.display_avatar.url
)
embed.timestamp = message.created_at
# Jump to message button
view = View()
view.add_item(
discord.ui.Button(
label="Jump to Message",
url=message.jump_url,
style=discord.ButtonStyle.url,
)
)
embed_list = [embed]
# Add reply embed
if message.reference:
try:
reply_message = await msg_channel.fetch_message(
message.reference.message_id
)
reply_embed = discord.Embed(
title="Replying To",
description=reply_message.content,
color=Color.random(),
)
reply_embed.set_author(
name=reply_message.author.name,
icon_url=reply_message.author.display_avatar.url,
)
reply_embed.timestamp = reply_message.created_at
embed_list.insert(0, reply_embed)
except discord.errors.NotFound:
pass
# Send message
board_channel = await self.bot.fetch_channel(channel_id)
board_message = await board_channel.send(
content=f"**{react_count} {emoji}** | {message.author.mention} | <#{payload.channel_id}>",
embeds=embed_list,
view=view,
files=[
await attachment.to_file() for attachment in message.attachments
],
)
async with self.fireboard_pool.acquire() as sql:
# Insert message to DB
await sql.execute(
"INSERT INTO fireMessages (serverID, msgID, boardMsgID, reactionAmount) VALUES (?, ?, ?, ?)",
(
payload.guild_id,
payload.message_id,
board_message.id,
react_count,
),
)
await sql.commit()
await self.refresh_fire_lists()
except Exception as e:
raise e
finally:
self.locked_messages.remove(payload.message_id)
# Listen for message reaction clear
@commands.Cog.listener()
async def on_raw_reaction_clear(self, payload: discord.RawReactionClearEvent):
self.bot: discord.ext.commands.Bot
# Stop if this is a DM
if payload.guild_id is None:
return
# Lock system
if payload.message_id in self.locked_messages:
while payload.message_id in self.locked_messages:
await asyncio.sleep(0.5)
self.locked_messages.append(payload.message_id)
try:
# Only trigger if message is already in the fireboard DB
if payload.message_id in [message[1] for message in self.fire_messages]:
# Find server config
for server in self.fire_settings:
if server[0] == payload.guild_id:
channel_id = server[3]
# Get guild
try:
guild: discord.Guild = await self.bot.fetch_guild(payload.guild_id)
except discord.errors.NotFound:
return
# Get message channel
channel: discord.abc.GuildChannel = await guild.fetch_channel(
payload.channel_id
)
# Get our message
message: discord.Message = await channel.fetch_message(
payload.message_id
)
# See if board message is already present
for fire_message in self.fire_messages:
if fire_message[1] == message.id:
async with self.fireboard_pool.acquire() as sql:
try:
# Delete message
try:
channel: discord.TextChannel = (
await guild.fetch_channel(channel_id)
)
except discord.errors.NotFound:
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?",
(payload.guild_id,),
)
await sql.commit()
self.locked_messages.remove(payload.message_id)
return
board_message = await channel.fetch_message(
fire_message[2]
)
await board_message.delete()
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(message.id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
except discord.errors.NotFound:
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(message.id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
else:
return
except Exception as e:
raise e
finally:
self.locked_messages.remove(payload.message_id)
# Listen for specific emoji being cleared
@commands.Cog.listener()
async def on_raw_reaction_clear_emoji(
self, payload: discord.RawReactionClearEmojiEvent
):
self.bot: discord.ext.commands.Bot
# Stop if this is a DM
if payload.guild_id is None:
return
# Lock system
if payload.message_id in self.locked_messages:
while payload.message_id in self.locked_messages:
await asyncio.sleep(0.5)
self.locked_messages.append(payload.message_id)
try:
# Only trigger if message is already in the fireboard DB
if payload.message_id in [message[1] for message in self.fire_messages]:
for server in self.fire_settings:
if server[0] == payload.guild_id:
emoji = server[2]
channel_id = server[3]
# Only trigger if cleared emoji is our emoji
if str(payload.emoji) == emoji:
# Fetch server
try:
guild: discord.Guild = await self.bot.fetch_guild(
payload.guild_id
)
except discord.errors.NotFound:
return
# Get message channel
channel: discord.abc.GuildChannel = await guild.fetch_channel(
payload.channel_id
)
# See if board message is already present
for fire_message in self.fire_messages:
if fire_message[1] == payload.message_id:
async with self.fireboard_pool.acquire() as sql:
try:
# Fetch fireboard channel
try:
channel: discord.TextChannel = (
await guild.fetch_channel(channel_id)
)
except discord.errors.NotFound:
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?",
(payload.guild_id,),
)
await sql.commit()
return
# Delete message
board_message = await channel.fetch_message(
fire_message[2]
)
await board_message.delete()
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(payload.message_id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
except discord.errors.NotFound:
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(payload.message_id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
else:
return
except Exception as e:
raise e
finally:
self.locked_messages.remove(payload.message_id)
# Listen for message being deleted
@commands.Cog.listener()
async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent):
self.bot: discord.ext.commands.Bot
# Stop if this is a DM
if payload.guild_id is None:
return
# Lock system
if payload.message_id in self.locked_messages:
while payload.message_id in self.locked_messages:
await asyncio.sleep(0.5)
self.locked_messages.append(payload.message_id)
try:
# Only trigger if message is already in the fireboard DB
if payload.message_id in [message[1] for message in self.fire_messages]:
# Fetch server config
for server in self.fire_settings:
if server[0] == payload.guild_id:
channel_id = server[3]
# Fetch server
try:
guild: discord.Guild = await self.bot.fetch_guild(payload.guild_id)
except discord.errors.NotFound:
return
# Get message channel
channel: discord.abc.GuildChannel = await guild.fetch_channel(
payload.channel_id
)
# See if board message is already present
for fire_message in self.fire_messages:
if fire_message[1] == payload.message_id:
async with self.fireboard_pool.acquire() as sql:
try:
# Fetch fireboard channel
try:
channel: discord.TextChannel = (
await guild.fetch_channel(channel_id)
)
except discord.errors.NotFound:
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?",
(payload.guild_id,),
)
await sql.commit()
return
# Delete message
board_message = await channel.fetch_message(
fire_message[2]
)
await board_message.delete()
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(payload.message_id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
except discord.errors.NotFound:
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(payload.message_id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
else:
return
except Exception as e:
raise e
finally:
self.locked_messages.remove(payload.message_id)
# Listen for message being edited
@commands.Cog.listener()
async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent):
self.bot: discord.ext.commands.Bot
# Stop if this is a DM
if payload.guild_id is None:
return
# Lock system
if payload.message_id in self.locked_messages:
while payload.message_id in self.locked_messages:
await asyncio.sleep(0.5)
self.locked_messages.append(payload.message_id)
try:
# Only trigger if message is already in the fireboard DB
if payload.message_id in [message[1] for message in self.fire_messages]:
# Fetch server config
for server in self.fire_settings:
if server[0] == payload.guild_id:
channel_id = server[3]
# Fetch server
try:
guild: discord.Guild = await self.bot.fetch_guild(payload.guild_id)
except discord.errors.NotFound:
return
# Get message channel
channel: discord.abc.GuildChannel = await guild.fetch_channel(
payload.channel_id
)
# Get our message
message: discord.Message = await channel.fetch_message(
payload.message_id
)
embed = discord.Embed(description=message.content, color=Color.random())
embed.set_author(
name=message.author.name, icon_url=message.author.display_avatar.url
)
embed.timestamp = message.created_at
# Jump to message button
view = View()
view.add_item(
discord.ui.Button(
label="Jump to Message",
url=message.jump_url,
style=discord.ButtonStyle.url,
)
)
embed_list = [embed]
# Add reply embed
if message.reference:
try:
reply_message = await channel.fetch_message(
message.reference.message_id
)
reply_embed = discord.Embed(
title="Replying To",
description=reply_message.content,
color=Color.random(),
)
reply_embed.set_author(
name=reply_message.author.name,
icon_url=reply_message.author.display_avatar.url,
)
reply_embed.timestamp = reply_message.created_at
embed_list.insert(0, reply_embed)
except discord.errors.NotFound:
pass
try:
channel: discord.TextChannel = await guild.fetch_channel(channel_id)
except discord.errors.NotFound:
async with self.fireboard_pool.acquire() as sql:
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?",
(payload.guild_id,),
)
await sql.commit()
return
# Find previous fireboard message
try:
for fire_message in self.fire_messages:
if (
fire_message[0] == payload.guild_id
and fire_message[1] == payload.message_id
):
# Edit with updated embed - reaction amount stays the same
board_message = await channel.fetch_message(fire_message[2])
await board_message.edit(
embeds=embed_list, attachments=message.attachments
)
except discord.errors.NotFound: # Message not found
async with self.fireboard_pool.acquire() as sql:
# Delete message from DB
await sql.execute(
"DELETE FROM fireMessages WHERE msgID = ?",
(payload.message_id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
else:
return
except Exception as e:
raise e
finally:
self.locked_messages.remove(payload.message_id)
# Listen for fireboard channel delete
@commands.Cog.listener()
async def on_guild_channel_delete(self, channel: discord.abc.GuildChannel):
# Only trigger if server has fireboard enabled
if channel.guild.id in [guild[0] for guild in self.fire_settings]:
for server in self.fire_settings:
if server[0] == channel.guild.id:
if server[3] == channel.id:
async with self.fireboard_pool.acquire() as sql:
# Delete fireboard config
await sql.execute(
"DELETE FROM fireMessages WHERE serverID = ?",
(channel.guild.id,),
)
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?",
(channel.guild.id,),
)
await sql.commit()
await self.refresh_fire_lists()
return
else:
return
# Listen for server being left / deleted
@commands.Cog.listener()
async def on_guild_remove(self, guild: discord.Guild):
# Only trigger if server has fireboard enabled
if guild.id in [guild[0] for guild in self.fire_settings]:
for server in self.fire_settings:
if server[0] == guild.id:
async with self.fireboard_pool.acquire() as sql:
# Delete fireboard config
await sql.execute(
"DELETE FROM fireMessages WHERE serverID = ?", (guild.id,)
)
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?", (guild.id,)
)
await sql.commit()
await self.refresh_fire_lists()
return
else:
return
# Command group setup
context = discord.app_commands.AppCommandContext(
guild=True, dm_channel=False, private_channel=False
)
installs = discord.app_commands.AppInstallationType(guild=True, user=False)
fireGroup = app_commands.Group(
name="fireboard",
description="Fireboard related commands.",
allowed_contexts=context,
allowed_installs=installs,
)
# Random fireboard message command
@fireGroup.command(
name="random", description="Get a random message from the fireboard."
)
@app_commands.describe(
ephemeral="Optional: whether to send the command output as a dismissible message only visible to you. Defaults to false."
)
async def random_fireboard(
self, interaction: discord.Interaction, ephemeral: bool = False
):
await interaction.response.defer(ephemeral=ephemeral)
channel_id = None
# Find server config
for server in self.fire_settings:
if server[0] == interaction.guild_id:
channel_id = server[3]
if channel_id is None:
embed = discord.Embed(
title="Error",
description="Fireboard is not enabled in this server.",
color=Color.red(),
)
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
return
# Fetch channel
try:
channel = await interaction.guild.fetch_channel(channel_id)
except discord.errors.NotFound:
embed = discord.Embed(
title="Error",
description="Can't find the fireboard channel. Please contact a server admin.",
color=Color.red(),
)
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
return
# Fetch messages
async with self.fireboard_pool.acquire() as sql:
messages = await sql.fetchall(
"SELECT * FROM fireMessages WHERE serverID = ?", (interaction.guild_id,)
)
if not messages:
embed = discord.Embed(
title="Error",
description="No messages found in the fireboard.",
color=Color.red(),
)
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
return
else:
while messages != []:
message = random.choice(messages)
try:
board_message = await channel.fetch_message(message[2])
view = View().from_message(board_message)
files = [
await attachment.to_file()
for attachment in board_message.attachments
]
await interaction.followup.send(
content=board_message.content,
embeds=board_message.embeds,
view=view,
ephemeral=ephemeral,
files=files,
allowed_mentions=discord.AllowedMentions.none(),
)
return
except discord.errors.NotFound:
messages.remove(message)
embed = discord.Embed(
title="Error",
description="No messages found in the fireboard.",
color=Color.red(),
)
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
# Command group setup
context = discord.app_commands.AppCommandContext(
guild=True, dm_channel=False, private_channel=False
)
installs = discord.app_commands.AppInstallationType(guild=True, user=False)
perms = discord.Permissions(manage_guild=True)
fireSetupGroup = app_commands.Group(
name="fireboard-setup",
description="Control the fireboard.",
allowed_contexts=context,
allowed_installs=installs,
default_permissions=perms,
)
# Fireboard enable command
@fireSetupGroup.command(
name="enable", description="Enable the fireboard in the current channel."
)
async def enable_fireboard(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild.id in [guild[0] for guild in self.fire_settings]:
embed = discord.Embed(
title="Fireboard is already enabled.", color=Color.green()
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
# Default settings
react_minimum = 3
emoji = "🔥"
channel_id = interaction.channel_id
ignore_bots = True
embed = discord.Embed(
title="Fireboard",
description="This channel has been configured as the server fireboard.",
color=Color.random(),
)
embed.set_footer(text="Feel free to delete this message!")
try:
channel = await interaction.guild.fetch_channel(channel_id)
await channel.send(embed=embed)
except discord.errors.Forbidden or discord.errors.NotFound:
embed = discord.Embed(
title="Error",
description="Looks like I can't send messages in this channel. Check permissions and try again.",
color=Color.random(),
)
await interaction.followup.send(embed=embed, ephemeral=True)
return
async with self.fireboard_pool.acquire() as sql:
# Insert to DB, refresh lists
await sql.execute(
"INSERT INTO fireSettings (serverID, reactionAmount, emoji, channelID, ignoreBots) VALUES (?, ?, ?, ?, ?)",
(
interaction.guild_id,
react_minimum,
emoji,
channel_id,
ignore_bots,
),
)
await sql.commit()
await self.refresh_fire_lists()
embed = discord.Embed(
title="Enabled",
description="Fireboard has been enabled in the current channel.",
color=Color.green(),
)
embed.add_field(
name="Info",
value=f"**Reaction Requirement:** `{react_minimum} reactions`\n**Fireboard Channel:** <#{channel_id}>\n**Emoji:** {emoji}\n**Ignore Bots:** `{ignore_bots}`",
)
await interaction.followup.send(embed=embed, ephemeral=True)
class ConfirmDisableView(View):
def __init__(self):
super().__init__(timeout=60)
async def disable_fireboard(
self, interaction: discord.Interaction, pool: asqlite.Pool
):
async with pool.acquire() as sql:
try:
await sql.execute(
"DELETE FROM fireMessages WHERE serverID = ?",
(interaction.guild_id,),
)
await sql.execute(
"DELETE FROM fireSettings WHERE serverID = ?",
(interaction.guild_id,),
)
await sql.execute(
"DELETE FROM fireChannelBlacklist WHERE serverID = ?",
(interaction.guild_id,),
)
await sql.execute(
"DELETE FROM fireRoleBlacklist WHERE serverID = ?",
(interaction.guild_id,),
)
await sql.commit()
return True
except Exception:
return False
@discord.ui.button(label="Disable", style=discord.ButtonStyle.red)
async def confirm(
self, interaction: discord.Interaction, button: discord.ui.Button
):
await interaction.response.defer(ephemeral=True)
success = await self.disable_fireboard(interaction, self.pool)
if success:
embed = discord.Embed(
title="Done!",
description="Fireboard was disabled.",
color=Color.green(),
)
await self.cog.refresh_fire_lists() # pylint: disable=no-member
else:
embed = discord.Embed(
title="Error",
description="Failed to disable fireboard.",
color=Color.red(),
)
await interaction.edit_original_response(embed=embed, view=None)
self.stop()
async def on_timeout(self):
for item in self.children:
item.disabled = True
embed = discord.Embed(
title="Timeout",
description="You didn't press the button in time.",
color=Color.red(),
)
await self.message.edit(embed=embed, view=self)
# Fireboard disable command
@fireSetupGroup.command(name="disable", description="Disable the server fireboard.")
async def disable_fireboard(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
if interaction.guild_id in [guild[0] for guild in self.fire_settings]:
view = self.ConfirmDisableView()
view.pool = self.fireboard_pool
view.cog = self
embed = discord.Embed(
title="Are you sure?",
description="All data about this server's fireboard will be deleted. This cannot be undone!",
color=Color.orange(),
)
message = await interaction.followup.send(
embed=embed, view=view, ephemeral=True, wait=True
)
view.message = message
else:
await interaction.followup.send(
"Fireboard is not enabled in this server!", ephemeral=True
)
# Fireboard server info command
@fireSetupGroup.command(
name="info", description="View fireboard config for this server."
)
async def fireboard_info(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild.id in [guild[0] for guild in self.fire_settings]:
# Fetch server settings
for server in self.fire_settings:
if server[0] == interaction.guild_id:
react_minimum = server[1]
emoji = server[2]
channel_id = server[3]
ignore_bots = True if int(server[4]) == 1 else False
embed = discord.Embed(
title="Server Fireboard Settings",
description=f"**Reaction Requirement:** `{react_minimum} reactions`\n**Fireboard Channel:** <#{channel_id}>\n**Emoji:** {emoji}\n**Ignore Bots:** `{ignore_bots}`",
color=Color.random(),
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
# Fireboard set emoji command
@fireSetupGroup.command(name="emoji", description="Set a custom fireboard emoji.")
async def fireboard_emoji(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=False)
# Check fireboard status
if interaction.guild.id in [guild[0] for guild in self.fire_settings]:
embed = discord.Embed(
title="Waiting for Reaction",
description="🔄 React with this message with your target emoji to set the fireboard emoji.",
color=Color.orange(),
)
msg = await interaction.followup.send(embed=embed, ephemeral=False)
def check(reaction, user):
return user == interaction.user and reaction.message.id == msg.id
# Wait for a reaction
try:
reaction, user = await self.bot.wait_for(
"reaction_add", timeout=60.0, check=check
)
reaction: discord.Reaction = reaction
async with self.fireboard_pool.acquire() as sql:
# Change emoji in DB, refresh lists
await sql.execute(
"UPDATE fireSettings SET emoji = ? WHERE serverID = ?",
(
str(reaction.emoji),
interaction.guild_id,
),
)
await sql.commit()
embed = discord.Embed(
title="Emoji Set",
description=f"Set emoji to **{str(reaction.emoji)}.**",
color=Color.green(),
)
await self.refresh_fire_lists()
await interaction.edit_original_response(embed=embed)
except asyncio.TimeoutError: # Timed out
embed = discord.Embed(
title="Timed Out",
description="You didn't react in time.",
color=Color.red(),
)
await interaction.edit_original_response(embed=embed)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=False)
# Fireboard set channel command
@fireSetupGroup.command(
name="channel",
description="Set the channel for fireboard messages to be sent in.",
)
async def fireboard_channel(
self, interaction: discord.Interaction, channel: discord.TextChannel
):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild.id in [guild[0] for guild in self.fire_settings]:
embed = discord.Embed(
title="Fireboard",
description="This channel has been configured as the server fireboard.",
color=Color.random(),
)
embed.set_footer(text="Feel free to delete this message!")
try:
await channel.send(embed=embed)
except discord.errors.NotFound:
embed = discord.Embed(
title="Error",
description="Looks like I can't find that channel. Check permissions and try again.",
color=Color.random(),
)
await interaction.followup.send(embed=embed, ephemeral=True)
return
except discord.errors.Forbidden as e:
embed = discord.Embed(
title="Error",
description="Looks like I can't send messages in that channel. Check permissions and try again.",
color=Color.red(),
)
await interaction.followup.send(embed=embed, ephemeral=True)
return
async with self.fireboard_pool.acquire() as sql:
# Update channel in DB, refresh lists
await sql.execute(
"UPDATE fireSettings SET channelID = ? WHERE serverID = ?",
(
channel.id,
interaction.guild_id,
),
)
await sql.commit()
await self.refresh_fire_lists()
embed = discord.Embed(
title="Channel Set",
description=f"Fireboard channel has been set to **{channel.mention}.**",
color=Color.green(),
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
# Fireboard set requirement command
@fireSetupGroup.command(
name="requirement",
description="Set required reaction amount for message to be posted on the fireboard.",
)
async def fireboard_requirement(
self, interaction: discord.Interaction, amount: int
):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild.id in [guild[0] for guild in self.fire_settings]:
embed = discord.Embed(
title="Set",
description=f"Reaction requirement has been set to **{amount} reactions.**",
color=Color.green(),
)
async with self.fireboard_pool.acquire() as sql:
# Update reaction requirement in DB, refresh lists
await sql.execute(
"UPDATE fireSettings SET reactionAmount = ? WHERE serverID = ?",
(
amount,
interaction.guild_id,
),
)
await sql.commit()
await self.refresh_fire_lists()
await interaction.followup.send(embed=embed, ephemeral=True)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
# Fireboard ignore bots command
@fireSetupGroup.command(
name="ignore-bots",
description="Whether bot messages are ignored in the fireboard. Defaults to true.",
)
async def fireboard_ignore_bots(
self, interaction: discord.Interaction, value: bool
):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild.id in [guild[0] for guild in self.fire_settings]:
embed = discord.Embed(
title="Set",
description=f"Bot messages will **{'be ignored.' if value else 'not be ignored.'}**",
color=Color.green(),
)
async with self.fireboard_pool.acquire() as sql:
# Update setting in DB, refresh lists
await sql.execute(
"UPDATE fireSettings SET ignoreBots = ? WHERE serverID = ?",
(
value,
interaction.guild_id,
),
)
await sql.commit()
await self.refresh_fire_lists()
await interaction.followup.send(embed=embed, ephemeral=True)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
# Fireboard role blacklist
@fireSetupGroup.command(
name="channel-blacklist",
description="Toggle the blacklist for a channel. NSFW channels are always blacklisted.",
)
async def fireboard_channel_blacklist(
self, interaction: discord.Interaction, channel: discord.abc.GuildChannel
):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild_id in [guild[0] for guild in self.fire_settings]:
async with self.fireboard_pool.acquire() as sql:
if channel.id in [
channelEntry[1] for channelEntry in self.fire_channel_blacklist
]:
await sql.execute(
"DELETE FROM fireChannelBlacklist WHERE serverID = ? AND channelID = ?",
(
interaction.guild_id,
channel.id,
),
)
await sql.commit()
await self.refresh_fire_lists()
embed = discord.Embed(
title="Set",
description=f"Removed {channel.mention} from the channel blacklist.",
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await sql.execute(
"INSERT INTO fireChannelBlacklist (serverID, channelID) VALUES (?, ?)",
(
interaction.guild_id,
channel.id,
),
)
await sql.commit()
await self.refresh_fire_lists()
embed = discord.Embed(
title="Set",
description=f"Added {channel.mention} to the channel blacklist.",
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
# Fireboard role blacklist
@fireSetupGroup.command(
name="role-blacklist", description="Toggle the blacklist for a role."
)
async def fireboard_role_blacklist(
self, interaction: discord.Interaction, role: discord.Role
):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild_id in [guild[0] for guild in self.fire_settings]:
async with self.fireboard_pool.acquire() as sql:
if role.id in [roleEntry[1] for roleEntry in self.fire_role_blacklist]:
await sql.execute(
"DELETE FROM fireRoleBlacklist WHERE serverID = ? AND roleID = ?",
(
interaction.guild_id,
role.id,
),
)
await sql.commit()
await self.refresh_fire_lists()
embed = discord.Embed(
title="Set",
description=f"Removed {role.mention} from the role blacklist.",
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await sql.execute(
"INSERT INTO fireRoleBlacklist (serverID, roleID) VALUES (?, ?)",
(
interaction.guild_id,
role.id,
),
)
await sql.commit()
await self.refresh_fire_lists()
embed = discord.Embed(
title="Set",
description=f"Added {role.mention} to the role blacklist.",
)
await interaction.followup.send(embed=embed, ephemeral=True)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
# Fireboard role blacklist
@fireSetupGroup.command(
name="blacklists", description="View this server's role and channel blacklists."
)
async def fireboard_blacklists(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
# Check fireboard status
if interaction.guild_id in [guild[0] for guild in self.fire_settings]:
class BlacklistViewer(View):
def __init__(self):
super().__init__(timeout=240)
self.fire_channel_blacklist: list
self.fire_role_blacklist: list
self.interaction: discord.Interaction
async def on_timeout(self) -> None:
for item in self.children:
item.disabled = True
await self.interaction.edit_original_response(view=self)
@discord.ui.button(
label="Role Blacklist",
style=discord.ButtonStyle.gray,
row=0,
custom_id="role",
disabled=True,
)
async def role(
self, interaction: discord.Interaction, button: discord.ui.Button
):
await interaction.response.defer(ephemeral=True)
for item in self.children:
if item.custom_id == "channel":
item.disabled = False
else:
item.disabled = True
my_roles = []
for role in self.fire_role_blacklist:
if role[0] == interaction.guild_id:
my_roles.append(f"<@&{role[1]}>")
if my_roles != []:
embed = discord.Embed(
title="Role Blacklist",
description="\n".join(my_roles),
color=Color.random(),
)
await interaction.edit_original_response(embed=embed, view=self)
else:
embed = discord.Embed(
title="Role Blacklist",
description="No roles have been blacklisted.",
color=Color.random(),
)
await interaction.edit_original_response(embed=embed, view=self)
@discord.ui.button(
label="Channel Blacklist",
style=discord.ButtonStyle.gray,
row=0,
custom_id="channel",
)
async def channel(
self, interaction: discord.Interaction, button: discord.ui.Button
):
await interaction.response.defer(ephemeral=True)
for item in self.children:
if item.custom_id == "role":
item.disabled = False
else:
item.disabled = True
my_channels = []
for channel in self.fire_channel_blacklist:
if channel[0] == interaction.guild_id:
my_channels.append(f"<#{channel[1]}>")
if my_channels != []:
embed = discord.Embed(
title="Channel Blacklist",
description="\n".join(my_channels),
color=Color.random(),
)
await interaction.edit_original_response(embed=embed, view=self)
else:
embed = discord.Embed(
title="Channel Blacklist",
description="No channels have been blacklisted.",
color=Color.random(),
)
await interaction.edit_original_response(embed=embed, view=self)
view_instance = BlacklistViewer()
view_instance.fire_channel_blacklist = self.fire_channel_blacklist
view_instance.fire_role_blacklist = self.fire_role_blacklist
view_instance.interaction = interaction
my_roles = []
for role in self.fire_role_blacklist:
if role[0] == interaction.guild_id:
my_roles.append(f"<@&{role[1]}>")
if my_roles != []:
embed = discord.Embed(
title="Role Blacklist",
description="\n".join(my_roles),
color=Color.random(),
)
await interaction.followup.send(
embed=embed, view=view_instance, ephemeral=True
)
else:
embed = discord.Embed(
title="Role Blacklist",
description="No roles have been blacklisted.",
color=Color.random(),
)
await interaction.followup.send(
embed=embed, view=view_instance, ephemeral=True
)
else:
embed = discord.Embed(title="Fireboard is not enabled.", color=Color.red())
await interaction.followup.send(embed=embed, ephemeral=True)
async def setup(bot):import asyncio
import random
import json
import os
import discord
from discord import Color, app_commands
from discord.ext import commands
from discord.ui import View
FIREBOARD_PATH = "assets/cogs/files/fireboard.json"
def load_data():
if not os.path.exists(FIREBOARD_PATH):
return {}
with open(FIREBOARD_PATH, "r") as f:
return json.load(f)
def save_data(data):
os.makedirs(os.path.dirname(FIREBOARD_PATH), exist_ok=True)
with open(FIREBOARD_PATH, "w") as f:
json.dump(data, f, indent=2)
class Fireboard(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.data = load_data()
self.locked_messages = []
def save(self):
save_data(self.data)
def get_guild(self, guild_id):
return self.data.setdefault(str(guild_id), {
"settings": None,
"messages": [],
"role_blacklist": [],
"channel_blacklist": []
})
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
if payload.guild_id is None:
return
guild_data = self.get_guild(payload.guild_id)
settings = guild_data["settings"]
if not settings:
return
react_minimum = settings["reactionAmount"]
emoji = settings["emoji"]
channel_id = settings["channelID"]
ignore_bots = settings["ignoreBots"]
if str(payload.emoji) != emoji:
return
if payload.channel_id in guild_data["channel_blacklist"]:
return
if payload.message_id in self.locked_messages:
return
self.locked_messages.append(payload.message_id)
try:
channel = await self.bot.fetch_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
if ignore_bots and message.author.bot:
return
if hasattr(message.channel, "nsfw") and message.channel.nsfw:
return
react_count = 0
for reaction in message.reactions:
if str(reaction.emoji) == emoji:
react_count = reaction.count
break
if react_count < react_minimum:
return
# Already on fireboard?
for m in guild_data["messages"]:
if m["msgID"] == payload.message_id:
return
# Send to fireboard
board_channel = await self.bot.fetch_channel(channel_id)
embed = discord.Embed(description=message.content, color=Color.random())
embed.set_author(name=message.author.name, icon_url=message.author.display_avatar.url)
embed.timestamp = message.created_at
view = View()
view.add_item(discord.ui.Button(label="Jump to Message", url=message.jump_url, style=discord.ButtonStyle.url))
files = [await a.to_file() for a in message.attachments]
board_message = await board_channel.send(
content=f"**{react_count} {emoji}** | {message.author.mention} | <#{payload.channel_id}>",
embed=embed, view=view, files=files
)
guild_data["messages"].append({
"msgID": payload.message_id,
"boardMsgID": board_message.id,
"reactionAmount": react_count
})
self.save()
finally:
if payload.message_id in self.locked_messages:
self.locked_messages.remove(payload.message_id)
@commands.Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
if payload.guild_id is None:
return
guild_data = self.get_guild(payload.guild_id)
settings = guild_data["settings"]
if not settings:
return
emoji = settings["emoji"]
channel_id = settings["channelID"]
if str(payload.emoji) != emoji:
return
for m in guild_data["messages"]:
if m["msgID"] == payload.message_id:
channel = await self.bot.fetch_channel(channel_id)
try:
board_message = await channel.fetch_message(m["boardMsgID"])
await board_message.delete()
except Exception:
pass
guild_data["messages"].remove(m)
self.save()
break
@app_commands.command(name="fireboard_enable", description="Enable the fireboard in this channel.")
async def fireboard_enable(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
guild_data = self.get_guild(interaction.guild_id)
if guild_data["settings"]:
await interaction.followup.send("Fireboard is already enabled.", ephemeral=True)
return
guild_data["settings"] = {
"reactionAmount": 3,
"emoji": "🔥",
"channelID": interaction.channel_id,
"ignoreBots": True
}
self.save()
await interaction.followup.send("Fireboard enabled in this channel.", ephemeral=True)
@app_commands.command(name="fireboard_disable", description="Disable the fireboard.")
async def fireboard_disable(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
guild_data = self.get_guild(interaction.guild_id)
guild_data["settings"] = None
guild_data["messages"] = []
self.save()
await interaction.followup.send("Fireboard disabled.", ephemeral=True)
@app_commands.command(name="fireboard_info", description="Show fireboard settings.")
async def fireboard_info(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
guild_data = self.get_guild(interaction.guild_id)
settings = guild_data["settings"]
if not settings:
await interaction.followup.send("Fireboard is not enabled.", ephemeral=True)
return
embed = discord.Embed(
title="Fireboard Settings",
description=f"**Reaction Requirement:** `{settings['reactionAmount']}`\n"
f"**Fireboard Channel:** <#{settings['channelID']}>\n"
f"**Emoji:** {settings['emoji']}\n"
f"**Ignore Bots:** `{settings['ignoreBots']}`",
color=Color.random()
)
await interaction.followup.send(embed=embed, ephemeral=True)
async def setup(bot):
await bot.add_cog(Fireboard(bot))