From e5a90a5ee048f81f1e81baac7a7a16d5303bb396 Mon Sep 17 00:00:00 2001 From: neoarz Date: Wed, 17 Sep 2025 23:05:08 -0400 Subject: [PATCH] feat(slop): tag system needs to be fixed --- bot.py | 14 + cogs/general/tags/__init__.py | 145 ++++++++++ cogs/general/tags/tagcreate.py | 16 ++ cogs/general/tags/tagdelete.py | 38 +++ cogs/general/tags/tagedit.py | 36 +++ cogs/general/tags/tagsend.py | 40 +++ cogs/general/tags/views/__init__.py | 0 cogs/general/tags/views/base.py | 15 ++ cogs/general/tags/views/models.py | 187 +++++++++++++ cogs/general/tags/views/tags.py | 398 ++++++++++++++++++++++++++++ database/schema.sql | 19 ++ 11 files changed, 908 insertions(+) create mode 100644 cogs/general/tags/__init__.py create mode 100644 cogs/general/tags/tagcreate.py create mode 100644 cogs/general/tags/tagdelete.py create mode 100644 cogs/general/tags/tagedit.py create mode 100644 cogs/general/tags/tagsend.py create mode 100644 cogs/general/tags/views/__init__.py create mode 100644 cogs/general/tags/views/base.py create mode 100644 cogs/general/tags/views/models.py create mode 100644 cogs/general/tags/views/tags.py diff --git a/bot.py b/bot.py index 5c22332..b0c6056 100644 --- a/bot.py +++ b/bot.py @@ -163,6 +163,20 @@ class DiscordBot(commands.Bot): self.logger.error( f"Failed to load extension {folder}.{extension}\n{exception}" ) + elif os.path.isdir(os.path.join(folder_path, file)) and not file.startswith('__'): + if os.path.exists(os.path.join(folder_path, file, "__init__.py")): + full_name = f"{folder}.{file}".lower() + if file.lower() in disabled_cogs or full_name in disabled_cogs: + self.logger.info(f"Skipped disabled extension '{full_name}'") + continue + try: + await self.load_extension(f"cogs.{folder}.{file}") + self.logger.info(f"Loaded extension '{folder}.{file}'") + except Exception as e: + exception = f"{type(e).__name__}: {e}" + self.logger.error( + f"Failed to load extension {folder}.{file}\n{exception}" + ) for file in os.listdir(cogs_path): if file.endswith(".py") and not file.startswith('__'): diff --git a/cogs/general/tags/__init__.py b/cogs/general/tags/__init__.py new file mode 100644 index 0000000..40c5d1f --- /dev/null +++ b/cogs/general/tags/__init__.py @@ -0,0 +1,145 @@ +from discord import Interaction, Embed, Color +from discord.ui import View +from discord.app_commands import Choice, Group, autocomplete, describe + +from .views.base import BaseCog +from .views.models import AsyncTagManager, Tag +from .views.tags import ( + AddTagButtonModal, + TagSelectButton, +) +from .tagsend import TagSend +from .tagcreate import TagCreate +from .tagedit import TagEdit +from .tagdelete import TagDelete + + +class Tags(BaseCog): + def __init__(self, bot): + super().__init__(bot) + self.description = "A cog for retrieving and setting tags." + + self._conn: AsyncTagManager | None = None + + # Initialize command classes + self.tag_send = TagSend(self) + self.tag_create = TagCreate(self) + self.tag_edit = TagEdit(self) + self.tag_delete = TagDelete(self) + + @property + def conn(self) -> AsyncTagManager: + if self._conn is None: raise ValueError("Initialized improperly!") + return self._conn + + @classmethod + async def setup(cls, bot): + import os + c = await super().setup(bot) + db_path = f"{os.path.realpath(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))}/database/database.db" + c._conn = await AsyncTagManager.from_file(db_path) + return c + + async def cog_unload(self): + if self._conn: + await self._conn.close() + await super().cog_unload() + + async def tag_completer(self, inter: Interaction, current: str): + _ = inter + if self._conn is None or inter.guild is None: + return [] + return [ + Choice(name=f"[G] {t.name}" if t.guild is None else t.name, value=str(t.tid)) + for t in await self._conn.tags + if current.lower() in t.name.lower() and (inter.guild.id == t.guild or t.guild is None) + ][:25] + + async def tag_button_completer(self, inter: Interaction, current: str): + _ = inter + if self._conn is None or inter.guild is None: + return [] + return [ + Choice(name=f"[G] {t.name}" if t.guild is None else t.name, value=str(t.tid)) + for t in await self._conn.tags + if current.lower() in t.name.lower() + and (inter.guild.id == t.guild or t.guild is None) + and len(t.buttons) > 0 + ][:25] + + tags = Group(name="tags", description="The parent for tag operations.", guild_only=True) + urls = Group(name="urls", description="Manage url buttons for tags.", parent=tags, guild_only=True) + + async def check_conn_tag(self, name: str) -> str | Tag: + if self._conn is None: + return "Error: couldn't connect to the db file to get tags!" + if not name.isnumeric(): + return f"Error: Tag {name!r} was not found!" + if (tag := await self._conn.tag(tid=int(name))) is None: + return f"Error: Tag with id {name!r} was not found!" + return tag + + @tags.command(description="Send contents of a tag.") + @describe(name="The name of the tag you want to send.") + @autocomplete(name=tag_completer) + async def send(self, inter: Interaction, name: str): + await self.tag_send.send(inter, name) + + @tags.command(description="Create a new tag.") + async def create(self, inter: Interaction): + await self.tag_create.create(inter) + + @tags.command(description="Edit a tag.") + @describe(name="The name of the tag you want to edit.") + @autocomplete(name=tag_completer) + async def edit(self, inter: Interaction, name: str): + await self.tag_edit.edit(inter, name) + + @tags.command(description="Delete a tag.") + @describe(name="The name of the tag you want to delete.") + @autocomplete(name=tag_completer) + async def delete(self, inter: Interaction, name: str): + await self.tag_delete.delete(inter, name) + + @urls.command(name="add", description="Add a url button to tag.") + @describe(name="The name of the tag you want to add a url button to.") + @autocomplete(name=tag_completer) + async def button_add(self, inter: Interaction, name: str): + tag = await self.check_conn_tag(name) + if isinstance(tag, str) or self._conn is None: + return await inter.response.send_message(tag, ephemeral=True) + await inter.response.send_modal(AddTagButtonModal(self, tag)) + + @urls.command(name="edit", description="Edit a button for a tag.") + @describe(name="The name of the tag you want to edit button from.") + @autocomplete(name=tag_button_completer) + async def button_edit(self, inter: Interaction, name: str): + tag = await self.check_conn_tag(name) + if isinstance(tag, str) or self._conn is None: + return await inter.response.send_message(tag, ephemeral=True) + try: + button_sel = TagSelectButton(self, tag) + button_view = View() + button_view.add_item(button_sel) + await inter.response.send_message(view=button_view, ephemeral=True) + except: + button_sel = TagSelectButton(self, tag, safe=True) + button_view = View() + button_view.add_item(button_sel) + await inter.response.send_message(view=button_view, ephemeral=True) + + @urls.command(name="delete", description="Delete button(s) for a tag.") + @describe(name="The name of the tag you want to delete button(s) from.") + @autocomplete(name=tag_button_completer) + async def button_delete(self, inter: Interaction, name: str): + tag = await self.check_conn_tag(name) + if isinstance(tag, str) or self._conn is None: + return await inter.response.send_message(tag, ephemeral=True) + button_sel = TagSelectButton(self, tag, True) + button_view = View() + button_view.add_item(button_sel) + await inter.response.send_message(view=button_view, ephemeral=True) + + +async def setup(bot): + await bot.add_cog(await Tags.setup(bot)) diff --git a/cogs/general/tags/tagcreate.py b/cogs/general/tags/tagcreate.py new file mode 100644 index 0000000..624118b --- /dev/null +++ b/cogs/general/tags/tagcreate.py @@ -0,0 +1,16 @@ +from discord import Interaction + +from .views.base import BaseCog +from .views.tags import CreateTagModal + + +class TagCreate: + def __init__(self, cog: BaseCog): + self.cog = cog + + async def create(self, inter: Interaction): + """Create a new tag.""" + if self.cog._conn is None: + await inter.response.send_message(f"Error: DB connection was None!", ephemeral=True) + return + await inter.response.send_modal(CreateTagModal(self.cog)) diff --git a/cogs/general/tags/tagdelete.py b/cogs/general/tags/tagdelete.py new file mode 100644 index 0000000..3607105 --- /dev/null +++ b/cogs/general/tags/tagdelete.py @@ -0,0 +1,38 @@ +import discord +from discord import Interaction + +from .views.base import BaseCog +from .views.models import AsyncTagManager, Tag +from .views.tags import ConfirmDeleteTag + + +class TagDelete: + def __init__(self, cog: BaseCog): + self.cog = cog + + async def check_conn_tag(self, name: str) -> str | Tag: + if self.cog._conn is None: + return "Error: couldn't connect to the db file to get tags!" + if not name.isnumeric(): + return f"Error: Tag {name!r} was not found!" + if (tag := await self.cog._conn.tag(tid=int(name))) is None: + return f"Error: Tag with id {name!r} was not found!" + return tag + + async def delete(self, inter: Interaction, name: str): + """Delete a tag.""" + tag = await self.check_conn_tag(name) + if isinstance(tag, str) or self.cog._conn is None: + return await inter.response.send_message(tag, ephemeral=True) + delete_tag = ConfirmDeleteTag(self.cog, tag) + embed = discord.Embed( + title="Confirm Deletion", + description=f"Are you sure you want to delete `{tag.name}`?", + color=0xFF0000 + ) + embed.set_author(name="Tags", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + await inter.response.send_message( + embed=embed, + view=delete_tag, + ephemeral=True, + ) diff --git a/cogs/general/tags/tagedit.py b/cogs/general/tags/tagedit.py new file mode 100644 index 0000000..71cbb09 --- /dev/null +++ b/cogs/general/tags/tagedit.py @@ -0,0 +1,36 @@ +from discord import Interaction, Embed + +from .views.base import BaseCog +from .views.models import AsyncTagManager, Tag +from .views.tags import EditTagPreview + + +class TagEdit: + def __init__(self, cog: BaseCog): + self.cog = cog + + async def check_conn_tag(self, name: str) -> str | Tag: + if self.cog._conn is None: + return "Error: couldn't connect to the db file to get tags!" + if not name.isnumeric(): + return f"Error: Tag {name!r} was not found!" + if (tag := await self.cog._conn.tag(tid=int(name))) is None: + return f"Error: Tag with id {name!r} was not found!" + return tag + + async def edit(self, inter: Interaction, name: str): + """Edit a tag.""" + tag = await self.check_conn_tag(name) + if isinstance(tag, str) or self.cog._conn is None: + return await inter.response.send_message(tag, ephemeral=True) + + embed = Embed( + title=f"Edit Tag: {tag.name}", + description=f"```\n{tag.content}\n```", + color=0x7289DA + ) + embed.set_author(name="Tags", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + embed.set_footer(text="Click Edit to modify this tag or Close to cancel") + + view = EditTagPreview(self.cog, tag) + await inter.response.send_message(embed=embed, view=view, ephemeral=True) diff --git a/cogs/general/tags/tagsend.py b/cogs/general/tags/tagsend.py new file mode 100644 index 0000000..c3a9714 --- /dev/null +++ b/cogs/general/tags/tagsend.py @@ -0,0 +1,40 @@ +from discord import Interaction + +from .views.base import BaseCog +from .views.models import AsyncTagManager, Tag +from .views.tags import TagEmbed, TagButtonView + + +class TagSend: + def __init__(self, cog: BaseCog): + self.cog = cog + + @property + def conn(self) -> AsyncTagManager: + return self.cog.conn + + async def check_conn_tag(self, name: str) -> str | Tag: + if self.cog._conn is None: + return "Error: couldn't connect to the db file to get tags!" + if not name.isnumeric(): + return f"Error: Tag {name!r} was not found!" + if (tag := await self.cog._conn.tag(tid=int(name))) is None: + return f"Error: Tag with id {name!r} was not found!" + return tag + + async def send(self, inter: Interaction, name: str): + """Send contents of a tag.""" + tag = await self.check_conn_tag(name) + if isinstance(tag, str) or self.cog._conn is None: + return await inter.response.send_message(tag, ephemeral=True) + ephemeral = inter.guild is None + author = self.cog.bot.get_user(tag.author) + authname = author.name if author else str(tag.author) + await inter.response.send_message( + embeds=[TagEmbed(tag, authname)], + view=TagButtonView(tag.buttons), + ephemeral=ephemeral + ) + tag.used += 1 + await self.cog._conn.update(tag) + self.cog.logger.info(f"{inter.user.name!r} sent {tag.name!r}") diff --git a/cogs/general/tags/views/__init__.py b/cogs/general/tags/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cogs/general/tags/views/base.py b/cogs/general/tags/views/base.py new file mode 100644 index 0000000..55dc084 --- /dev/null +++ b/cogs/general/tags/views/base.py @@ -0,0 +1,15 @@ +import logging +from discord.ext import commands + + +class BaseCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.logger = logging.getLogger(f"discord_bot.{self.__class__.__name__}") + + @classmethod + async def setup(cls, bot): + return cls(bot) + + async def cog_unload(self): + pass diff --git a/cogs/general/tags/views/models.py b/cogs/general/tags/views/models.py new file mode 100644 index 0000000..e3401d9 --- /dev/null +++ b/cogs/general/tags/views/models.py @@ -0,0 +1,187 @@ +import aiosqlite +import asyncio +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class TagButton: + id: int + tag_id: int + label: str + url: str + emoji: Optional[str] = None + + +@dataclass +class Tag: + tid: int + name: str + content: str + author: int + guild: Optional[int] + used: int = 0 + buttons: List[TagButton] = None + + def __post_init__(self): + if self.buttons is None: + self.buttons = [] + + +class AsyncTagManager: + def __init__(self, db_path: str): + self.db_path = db_path + self._connection = None + self._tags_cache = None + + @classmethod + async def from_file(cls, db_path: str): + manager = cls(db_path) + await manager._connect() + return manager + + async def _connect(self): + self._connection = await aiosqlite.connect(self.db_path) + await self._load_tags() + + async def close(self): + if self._connection: + await self._connection.close() + self._connection = None + + async def _load_tags(self): + if not self._connection: + return + + cursor = await self._connection.execute(""" + SELECT t.tid, t.name, t.content, t.author, t.guild, t.used, + b.id, b.label, b.url, b.emoji + FROM tags t + LEFT JOIN tag_buttons b ON t.tid = b.tag_id + ORDER BY t.tid + """) + + rows = await cursor.fetchall() + await cursor.close() + + tags_dict = {} + for row in rows: + tid = row[0] + if tid not in tags_dict: + tags_dict[tid] = Tag( + tid=row[0], + name=row[1], + content=row[2], + author=row[3], + guild=row[4], + used=row[5], + buttons=[] + ) + + if row[6] is not None: + button = TagButton( + id=row[6], + tag_id=tid, + label=row[7], + url=row[8], + emoji=row[9] + ) + tags_dict[tid].buttons.append(button) + + self._tags_cache = list(tags_dict.values()) + + @property + async def tags(self) -> List[Tag]: + if self._tags_cache is None: + await self._load_tags() + return self._tags_cache + + async def tag(self, tid: int = None, name: str = None) -> Optional[Tag]: + tags = await self.tags + if tid is not None: + return next((tag for tag in tags if tag.tid == tid), None) + if name is not None: + return next((tag for tag in tags if tag.name.lower() == name.lower()), None) + return None + + async def create_tag(self, name: str, content: str, author: int, guild: Optional[int] = None) -> Tag: + if not self._connection: + raise ValueError("Database not connected") + + cursor = await self._connection.execute( + "INSERT INTO tags (name, content, author, guild) VALUES (?, ?, ?, ?)", + (name, content, author, guild) + ) + await self._connection.commit() + + tid = cursor.lastrowid + await cursor.close() + + tag = Tag(tid=tid, name=name, content=content, author=author, guild=guild, used=0, buttons=[]) + self._tags_cache.append(tag) + return tag + + async def update(self, tag: Tag): + if not self._connection: + raise ValueError("Database not connected") + + await self._connection.execute( + "UPDATE tags SET name=?, content=?, used=? WHERE tid=?", + (tag.name, tag.content, tag.used, tag.tid) + ) + await self._connection.commit() + + if self._tags_cache: + for i, cached_tag in enumerate(self._tags_cache): + if cached_tag.tid == tag.tid: + self._tags_cache[i] = tag + break + + async def delete_tag(self, tag: Tag): + if not self._connection: + raise ValueError("Database not connected") + + await self._connection.execute("DELETE FROM tags WHERE tid=?", (tag.tid,)) + await self._connection.commit() + + if self._tags_cache: + self._tags_cache = [t for t in self._tags_cache if t.tid != tag.tid] + + async def add_button(self, tag: Tag, label: str, url: str, emoji: Optional[str] = None) -> TagButton: + if not self._connection: + raise ValueError("Database not connected") + + cursor = await self._connection.execute( + "INSERT INTO tag_buttons (tag_id, label, url, emoji) VALUES (?, ?, ?, ?)", + (tag.tid, label, url, emoji) + ) + await self._connection.commit() + + button_id = cursor.lastrowid + await cursor.close() + + button = TagButton(id=button_id, tag_id=tag.tid, label=label, url=url, emoji=emoji) + tag.buttons.append(button) + return button + + async def update_button(self, button: TagButton): + if not self._connection: + raise ValueError("Database not connected") + + await self._connection.execute( + "UPDATE tag_buttons SET label=?, url=?, emoji=? WHERE id=?", + (button.label, button.url, button.emoji, button.id) + ) + await self._connection.commit() + + async def delete_button(self, button: TagButton): + if not self._connection: + raise ValueError("Database not connected") + + await self._connection.execute("DELETE FROM tag_buttons WHERE id=?", (button.id,)) + await self._connection.commit() + + for tag in await self.tags: + if tag.tid == button.tag_id: + tag.buttons = [b for b in tag.buttons if b.id != button.id] + break diff --git a/cogs/general/tags/views/tags.py b/cogs/general/tags/views/tags.py new file mode 100644 index 0000000..b17a7c0 --- /dev/null +++ b/cogs/general/tags/views/tags.py @@ -0,0 +1,398 @@ +import discord +from discord import Interaction, ButtonStyle, SelectOption +from discord.ui import Modal, TextInput, View, Button, Select +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from .models import Tag, TagButton + +class TagEmbed(discord.Embed): + def __init__(self, tag: "Tag", author_name: str): + super().__init__( + description=tag.content, + color=0x7289DA, + timestamp=discord.utils.utcnow() + ) + self.set_author(name="Tags", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + self.set_footer(text=f"Created by {author_name} | Used {tag.used} times") + + +class TagButtonView(View): + def __init__(self, buttons: List["TagButton"]): + super().__init__(timeout=None) + for button in buttons[:5]: + self.add_item(TagUrlButton(button)) + + +class TagUrlButton(Button): + def __init__(self, tag_button: "TagButton"): + super().__init__( + label=tag_button.label, + url=tag_button.url, + emoji=tag_button.emoji, + style=ButtonStyle.link + ) + + +class CreateTagModal(Modal): + def __init__(self, cog): + super().__init__(title="Create New Tag") + self.cog = cog + + self.name_input = TextInput( + label="Tag Name", + placeholder="Enter the name for your tag...", + max_length=100, + required=True + ) + self.content_input = TextInput( + label="Tag Content", + placeholder="Enter the content for your tag...", + style=discord.TextStyle.paragraph, + max_length=2000, + required=True + ) + + self.add_item(self.name_input) + self.add_item(self.content_input) + + async def on_submit(self, interaction: Interaction): + name = self.name_input.value + content = self.content_input.value + author = interaction.user.id + guild = interaction.guild.id if interaction.guild else None + + existing_tag = await self.cog.conn.tag(name=name) + if existing_tag and (existing_tag.guild == guild or existing_tag.guild is None): + await interaction.response.send_message( + f"A tag with the name '{name}' already exists!", + ephemeral=True + ) + return + + try: + tag = await self.cog.conn.create_tag(name, content, author, guild) + embed = discord.Embed( + title="Tag Created!", + description=f"Successfully created tag `{name}`!", + color=0x7289DA + ) + embed.set_author(name="Tags", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + await interaction.response.send_message(embed=embed, ephemeral=True) + self.cog.logger.info(f"Tag '{name}' created by {interaction.user.name}") + except Exception as e: + await interaction.response.send_message( + f"An error occurred while creating the tag: {str(e)}", + ephemeral=True + ) + + +class EditTagPreview(View): + def __init__(self, cog, tag: "Tag"): + super().__init__(timeout=300) + self.cog = cog + self.tag = tag + + @discord.ui.button(label="Edit", style=ButtonStyle.primary) + async def edit_button(self, interaction: Interaction, button: Button): + if interaction.user.id != self.tag.author and not interaction.user.guild_permissions.manage_messages: + await interaction.response.send_message( + "You don't have permission to edit this tag!", + ephemeral=True + ) + return + await interaction.response.send_modal(EditTagModal(self.cog, self.tag)) + + @discord.ui.button(label="Close", style=ButtonStyle.danger) + async def close_button(self, interaction: Interaction, button: Button): + await interaction.response.edit_message(content="Tag edit cancelled.", embed=None, view=None) + + +class EditTagModal(Modal): + def __init__(self, cog, tag: "Tag"): + super().__init__(title=f"Edit Tag: {tag.name}") + self.cog = cog + self.tag = tag + + self.name_input = TextInput( + label="Tag Name", + default=tag.name, + max_length=100, + required=True + ) + self.content_input = TextInput( + label="Tag Content", + default=tag.content, + style=discord.TextStyle.paragraph, + max_length=2000, + required=True + ) + + self.add_item(self.name_input) + self.add_item(self.content_input) + + async def on_submit(self, interaction: Interaction): + if interaction.user.id != self.tag.author and not interaction.user.guild_permissions.manage_messages: + await interaction.response.send_message( + "You don't have permission to edit this tag!", + ephemeral=True + ) + return + + name = self.name_input.value + content = self.content_input.value + + guild = interaction.guild.id if interaction.guild else None + existing_tag = await self.cog.conn.tag(name=name) + if existing_tag and existing_tag.tid != self.tag.tid and (existing_tag.guild == guild or existing_tag.guild is None): + await interaction.response.send_message( + f"A tag with the name '{name}' already exists!", + ephemeral=True + ) + return + + try: + self.tag.name = name + self.tag.content = content + await self.cog.conn.update(self.tag) + embed = discord.Embed( + title="Success!", + description=f"Successfully updated tag `{name}`!", + color=0x00FF00 + ) + embed.set_author(name="Tags", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + await interaction.response.send_message(embed=embed, ephemeral=True) + self.cog.logger.info(f"Tag '{name}' updated by {interaction.user.name}") + except Exception as e: + await interaction.response.send_message( + f"An error occurred while updating the tag: {str(e)}", + ephemeral=True + ) + + +class ConfirmDeleteTag(View): + def __init__(self, cog, tag: "Tag"): + super().__init__(timeout=60) + self.cog = cog + self.tag = tag + + @discord.ui.button(label="Yes", style=ButtonStyle.danger) + async def confirm_delete(self, interaction: Interaction, button: Button): + if interaction.user.id != self.tag.author and not interaction.user.guild_permissions.manage_messages: + await interaction.response.send_message( + "You don't have permission to delete this tag!", + ephemeral=True + ) + return + + try: + await self.cog.conn.delete_tag(self.tag) + await interaction.response.edit_message( + embed=discord.Embed( + title="Success!", + description=f"Tag `{self.tag.name}` has been deleted.", + color=0x00FF00 + ), + view=None + ) + self.cog.logger.info(f"Tag '{self.tag.name}' deleted by {interaction.user.name}") + except Exception as e: + await interaction.response.send_message( + f"An error occurred while deleting the tag: {str(e)}", + ephemeral=True + ) + + @discord.ui.button(label="No", style=ButtonStyle.secondary) + async def cancel_delete(self, interaction: Interaction, button: Button): + await interaction.response.edit_message( + content="Tag deletion cancelled.", + view=None + ) + + +class AddTagButtonModal(Modal): + def __init__(self, cog, tag: "Tag"): + super().__init__(title=f"Add Button to: {tag.name}") + self.cog = cog + self.tag = tag + + self.label_input = TextInput( + label="Button Label", + placeholder="Enter the button label...", + max_length=80, + required=True + ) + self.url_input = TextInput( + label="Button URL", + placeholder="https://example.com", + max_length=512, + required=True + ) + self.emoji_input = TextInput( + label="Button Emoji (Optional)", + placeholder="Enter emoji...", + max_length=100, + required=False + ) + + self.add_item(self.label_input) + self.add_item(self.url_input) + self.add_item(self.emoji_input) + + async def on_submit(self, interaction: Interaction): + if interaction.user.id != self.tag.author and not interaction.user.guild_permissions.manage_messages: + await interaction.response.send_message( + "You don't have permission to modify this tag!", + ephemeral=True + ) + return + + if len(self.tag.buttons) >= 5: + await interaction.response.send_message( + "Tags can only have up to 5 buttons!", + ephemeral=True + ) + return + + label = self.label_input.value + url = self.url_input.value + emoji = self.emoji_input.value if self.emoji_input.value else None + + try: + await self.cog.conn.add_button(self.tag, label, url, emoji) + await interaction.response.send_message( + f"Successfully added button '{label}' to tag '{self.tag.name}'!", + ephemeral=True + ) + self.cog.logger.info(f"Button '{label}' added to tag '{self.tag.name}' by {interaction.user.name}") + except Exception as e: + await interaction.response.send_message( + f"An error occurred while adding the button: {str(e)}", + ephemeral=True + ) + + +class TagSelectButton(Select): + def __init__(self, cog, tag: "Tag", delete_mode: bool = False, safe: bool = False): + self.cog = cog + self.tag = tag + self.delete_mode = delete_mode + + options = [] + for i, button in enumerate(tag.buttons[:25]): + option_label = button.label + if len(option_label) > 100: + option_label = option_label[:97] + "..." + + options.append(SelectOption( + label=option_label, + value=str(button.id), + description=button.url[:100] if len(button.url) <= 100 else button.url[:97] + "...", + emoji=button.emoji if not safe else None + )) + + if not options: + options.append(SelectOption( + label="No buttons available", + value="none", + description="This tag has no buttons" + )) + + super().__init__( + placeholder="Select a button to edit..." if not delete_mode else "Select a button to delete...", + options=options, + disabled=len(tag.buttons) == 0 + ) + + async def callback(self, interaction: Interaction): + if interaction.user.id != self.tag.author and not interaction.user.guild_permissions.manage_messages: + await interaction.response.send_message( + "You don't have permission to modify this tag!", + ephemeral=True + ) + return + + if self.values[0] == "none": + await interaction.response.send_message( + "No buttons available to modify.", + ephemeral=True + ) + return + + button_id = int(self.values[0]) + button = next((b for b in self.tag.buttons if b.id == button_id), None) + + if not button: + await interaction.response.send_message( + "Button not found!", + ephemeral=True + ) + return + + if self.delete_mode: + try: + await self.cog.conn.delete_button(button) + await interaction.response.send_message( + f"Successfully deleted button '{button.label}' from tag '{self.tag.name}'!", + ephemeral=True + ) + self.cog.logger.info(f"Button '{button.label}' deleted from tag '{self.tag.name}' by {interaction.user.name}") + except Exception as e: + await interaction.response.send_message( + f"An error occurred while deleting the button: {str(e)}", + ephemeral=True + ) + else: + await interaction.response.send_modal(EditTagButtonModal(self.cog, self.tag, button)) + + +class EditTagButtonModal(Modal): + def __init__(self, cog, tag: "Tag", button: "TagButton"): + super().__init__(title=f"Edit Button: {button.label}") + self.cog = cog + self.tag = tag + self.button = button + + self.label_input = TextInput( + label="Button Label", + default=button.label, + max_length=80, + required=True + ) + self.url_input = TextInput( + label="Button URL", + default=button.url, + max_length=512, + required=True + ) + self.emoji_input = TextInput( + label="Button Emoji (Optional)", + default=button.emoji or "", + max_length=100, + required=False + ) + + self.add_item(self.label_input) + self.add_item(self.url_input) + self.add_item(self.emoji_input) + + async def on_submit(self, interaction: Interaction): + label = self.label_input.value + url = self.url_input.value + emoji = self.emoji_input.value if self.emoji_input.value else None + + try: + self.button.label = label + self.button.url = url + self.button.emoji = emoji + await self.cog.conn.update_button(self.button) + await interaction.response.send_message( + f"Successfully updated button '{label}' on tag '{self.tag.name}'!", + ephemeral=True + ) + self.cog.logger.info(f"Button '{label}' updated on tag '{self.tag.name}' by {interaction.user.name}") + except Exception as e: + await interaction.response.send_message( + f"An error occurred while updating the button: {str(e)}", + ephemeral=True + ) diff --git a/database/schema.sql b/database/schema.sql index 892c418..1c2edd4 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -7,4 +7,23 @@ CREATE TABLE IF NOT EXISTS `warns` ( `moderator_id` varchar(20) NOT NULL, `reason` varchar(255) NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `tags` ( + `tid` INTEGER PRIMARY KEY AUTOINCREMENT, + `name` varchar(255) NOT NULL, + `content` TEXT NOT NULL, + `author` varchar(20) NOT NULL, + `guild` varchar(20), + `used` INTEGER DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `tag_buttons` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `tag_id` INTEGER NOT NULL, + `label` varchar(80) NOT NULL, + `url` TEXT NOT NULL, + `emoji` varchar(100), + FOREIGN KEY (tag_id) REFERENCES tags(tid) ON DELETE CASCADE ); \ No newline at end of file