From 93a6f8713397c1d7ea6b702afb952c8cde5e72ff Mon Sep 17 00:00:00 2001 From: dnns01 <github@dnns01.de> Date: Tue, 9 May 2023 00:11:59 +0200 Subject: [PATCH] Change links to use db instead of json file and modals for creating/editing links and renaming categories. (#213) Also created json importer to import current data from json into database. Co-authored-by: dnns01 <git@dnns01.de> --- extensions/links.py | 206 +++++++++++++++---------------------------- json_import.py | 22 +++++ modals/link_modal.py | 83 +++++++++++++++++ models.py | 45 ++++++++++ requirements.txt | 4 +- views/poll_view.py | 4 +- 6 files changed, 227 insertions(+), 137 deletions(-) create mode 100644 json_import.py create mode 100644 modals/link_modal.py create mode 100644 models.py diff --git a/extensions/links.py b/extensions/links.py index a3fb160..47fde02 100644 --- a/extensions/links.py +++ b/extensions/links.py @@ -1,80 +1,71 @@ -import json - import discord from discord import app_commands, Interaction from discord.ext import commands +import models +from modals.link_modal import LinkModal, LinkCategoryModal + @app_commands.guild_only() class Links(commands.GroupCog, name="links", description="Linkverwaltung für Kanäle."): def __init__(self, bot): self.bot = bot - self.links = {} - self.links_file = "data/links.json" - self.load_links() - - def load_links(self): - links_file = open(self.links_file, 'r') - self.links = json.load(links_file) - - def save_links(self): - links_file = open(self.links_file, 'w') - json.dump(self.links, links_file) - - @app_commands.command(name="list", description="Liste Links für diesen Kanal auf.") - @app_commands.describe(topic="Zeige nur Links für dieses Thema an.", - public="Zeige die Ausgabe des Commands öffentlich, für alle Mitglieder sichtbar.") - async def cmd_list(self, interaction: Interaction, topic: str = None, public: bool = False): + + @app_commands.command(name="show", description="Zeige Links für diesen Kanal an.") + @app_commands.describe(category="Zeige nur Links für diese Kategorie an.", public="Zeige die Linkliste für alle.") + async def cmd_show(self, interaction: Interaction, category: str = None, public: bool = False): await interaction.response.defer(ephemeral=not public) - if channel_links := self.links.get(str(interaction.channel_id)): - embed = discord.Embed(title=f"Folgende Links sind in diesem Channel hinterlegt:\n") - if topic: - topic = topic.lower() - if topic_links := channel_links.get(topic): - value = f"" - for title, link in topic_links.items(): - value += f"- [{title}]({link})\n" - embed.add_field(name=topic.capitalize(), value=value, inline=False) - await interaction.edit_original_response(embed=embed) - else: - await interaction.edit_original_response( - content=f"Für das Thema `{topic}` sind in diesem Channel keine Links hinterlegt. Versuch es " - f"noch mal mit einem anderen Thema, oder lass dir mit `!links` alle Links in diesem " - f"Channel ausgeben") + embed = discord.Embed(title=f"Links") + if not models.LinkCategory.has_links(interaction.channel_id): + embed.description = "Für diesen Channel sind noch keine Links hinterlegt." + if category and not models.LinkCategory.has_links(interaction.channel_id, category=category): + embed.description = f"Für die Kategorie `{category}` sind in diesem Channel keine Links hinterlegt. " \ + f"Versuch es noch mal mit einer anderen Kategorie, oder lass dir mit `/links show` " \ + f"alle Links in diesem Channel ausgeben." + + for category in models.LinkCategory.get_categories(interaction.channel_id, category=category): + if category.links.count() > 0: + category.append_field(embed) else: - for topic, links in channel_links.items(): - value = f"" - for title, link in links.items(): - value += f"- [{title}]({link})\n" - embed.add_field(name=topic.capitalize(), value=value, inline=False) - await interaction.edit_original_response(embed=embed) - else: - await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") - - @app_commands.command(name="add", description="Füge einen neuen Link hinzu.") - @app_commands.describe(topic="Thema, zu dem dieser Link hinzugefügt werden soll.", - link="Link, der hinzugefügt werden soll.", title="Titel des Links.") - async def cmd_add(self, interaction: Interaction, topic: str, link: str, title: str): - await interaction.response.defer(ephemeral=True) - topic = topic.lower() - if not (channel_links := self.links.get(str(interaction.channel_id))): - self.links[str(interaction.channel_id)] = {} - channel_links = self.links.get(str(interaction.channel_id)) + category.delete_instance() - if not (topic_links := channel_links.get(topic)): - channel_links[topic] = {} - topic_links = channel_links.get(topic) + await interaction.edit_original_response(embed=embed) - self.add_link(topic_links, link, title) - self.save_links() - await interaction.edit_original_response(content="Link hinzugefügt.") + @app_commands.command(name="add", description="Füge einen neuen Link hinzu.") + async def cmd_add(self, interaction: Interaction): + await interaction.response.send_modal(LinkModal()) - def add_link(self, topic_links, link, title): - if topic_links.get(title): - self.add_link(topic_links, link, title + str(1)) + @app_commands.command(name="edit-link", description="Einen bestehenden Link in der Liste bearbeiten.") + @app_commands.describe(category="Kategorie zu der der zu bearbeitende Link gehört.", + title="Titel des zu bearbeitenden Links.") + async def cmd_edit_link(self, interaction: Interaction, category: str, title: str): + if db_category := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id, + models.LinkCategory.name == category): + if link := models.Link.get_or_none(models.Link.title == title, models.Link.category == db_category.id): + await interaction.response.send_modal( + LinkModal(category=link.category.name, link_title=link.title, link=link.link, link_id=link.id, + title="Link bearbeiten")) + else: + await interaction.response.send_message(content='Ich konnte den Link leider nicht finden.', + ephemeral=True) + else: + await interaction.response.send_message(content='Ich konnte die Kategorie leider nicht finden.', + ephemeral=True) + + @app_commands.command(name="rename-category", description="Kategorie bearbeiten.") + @app_commands.describe(category="Zu bearbeitende Kategorie") + async def cmd_rename_category(self, interaction: Interaction, category: str): + if not models.LinkCategory.has_links(interaction.channel_id): + await interaction.response.send_message(content="Für diesen Channel sind noch keine Links hinterlegt.", + ephemeral=True) + return + + if db_category := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id, + models.LinkCategory.name == category): + await interaction.response.send_modal(LinkCategoryModal(db_category=db_category)) else: - topic_links[title] = link + await interaction.response.send_message(content='Ich konnte das Thema leider nicht finden.', ephemeral=True) @app_commands.command(name="remove-link", description="Einen Link entfernen.") @app_commands.describe(topic="Theme zu dem der zu entfernende Link gehört.", @@ -83,21 +74,19 @@ class Links(commands.GroupCog, name="links", description="Linkverwaltung für Ka await interaction.response.defer(ephemeral=True) topic = topic.lower() - if channel_links := self.links.get(str(interaction.channel_id)): - if topic_links := channel_links.get(topic): - if title in topic_links: - topic_links.pop(title) - if not topic_links: - channel_links.pop(topic) - await interaction.edit_original_response(content="Link entfernt.") - else: - await interaction.edit_original_response(content='Ich konnte den Link leider nicht finden.') + if not models.LinkCategory.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + if topic_entity := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id, + models.LinkCategory.name == topic): + if link := models.Link.get_or_none(models.Link.title == title, models.Link.topic == topic_entity.id): + link.delete_instance(recursive=True) + await interaction.edit_original_response(content=f'Link {title} entfernt') else: - await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + await interaction.edit_original_response(content='Ich konnte den Link leider nicht finden.') else: - await interaction.edit_original_response(content='Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() + await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + return @app_commands.command(name="remove-topic", description="Ein Thema mit allen zugehörigen Links entfernen.") @app_commands.describe(topic="Zu entfernendes Thema.") @@ -105,67 +94,16 @@ class Links(commands.GroupCog, name="links", description="Linkverwaltung für Ka await interaction.response.defer(ephemeral=True) topic = topic.lower() - if channel_links := self.links.get(str(interaction.channel_id)): - if channel_links.get(topic): - channel_links.pop(topic) - await interaction.edit_original_response(content="Thema entfernt") - else: - await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') - else: - await interaction.edit_original_response(content='Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() - - @app_commands.command(name="edit-link", description="Einen bestehenden Link in der Liste bearbeiten.") - @app_commands.describe(topic="Thema zu dem der zu bearbeitende Link gehört.", - title="Titel des zu bearbeitenden Links.", new_title="Neuer Titel des Links.", - new_topic="Neues Thema des Links.", new_link="Neuer Link.") - async def cmd_edit_link(self, interaction: Interaction, topic: str, title: str, new_title: str, - new_topic: str = None, new_link: str = None): - await interaction.response.defer(ephemeral=True) - topic = topic.lower() - new_topic = new_topic.lower() if new_topic else topic - - if channel_links := self.links.get(str(interaction.channel_id)): - if topic_links := channel_links.get(topic): - if topic_links.get(title): - new_link = new_link if new_link else topic_links.get(title) - del topic_links[title] - else: - await interaction.edit_original_response(content='Ich konnte den Link leider nicht finden.') - return - else: - await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') - return - new_title = new_title if new_title else title - if topic_links := channel_links.get(new_topic): - topic_links[new_title] = new_link - else: - channel_links[new_topic] = {new_title: new_link} - await interaction.edit_original_response(content="Link erfolgreich editiert.") - else: - await interaction.edit_original_response(content='Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() - - @app_commands.command(name="edit-topic", description="Thema bearbeiten.") - @app_commands.describe(topic="Zu bearbeitendes Thema", new_topic="Neues Thema") - async def cmd_edit_topic(self, interaction: Interaction, topic: str, new_topic: str): - await interaction.response.defer(ephemeral=True) - topic = topic.lower() - new_topic = new_topic.lower() - - if channel_links := self.links.get(str(interaction.channel_id)): - if topic_links := channel_links.get(topic): - channel_links[new_topic] = topic_links - del channel_links[topic] - await interaction.edit_original_response(content="Thema aktualisiert.") - else: - await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + if not models.LinkCategory.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + if topic_entity := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id, + models.LinkCategory.name == topic): + topic_entity.delete_instance(recursive=True) + await interaction.edit_original_response(content=f'Thema {topic} mit allen zugehörigen Links entfernt') else: - await interaction.edit_original_response(content='Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() + await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + return async def setup(bot: commands.Bot) -> None: diff --git a/json_import.py b/json_import.py new file mode 100644 index 0000000..6e8406f --- /dev/null +++ b/json_import.py @@ -0,0 +1,22 @@ +import json + +import models + + +def import_links(json_file: str) -> None: + file = open(json_file, mode="r") + links = json.load(file) + + for channel, categories in links.items(): + for category, links in categories.items(): + category = category.capitalize() + db_category = models.LinkCategory.get_or_create(channel=int(channel), name=category) + for title, link in links.items(): + models.Link.create(link=link, title=title, category=db_category[0].id) + + +if __name__ == "__main__": + """ + Make sure to create a database backup before you import data from json files. + """ + # import_links("data/links.json") diff --git a/modals/link_modal.py b/modals/link_modal.py new file mode 100644 index 0000000..f557272 --- /dev/null +++ b/modals/link_modal.py @@ -0,0 +1,83 @@ +import re +import traceback +from typing import Optional + +import discord as discord +from discord import ui +from discord.utils import MISSING + +import models + + +class InvalidLinkError(Exception): + pass + + +class LinkDoesNotExistError(Exception): + pass + + +class LinkModal(ui.Modal, title='Link hinzufügen'): + def __init__(self, *, category: str = None, link_title: str = None, link: str = None, link_id: int = None, + title: str = MISSING, timeout: Optional[float] = None, custom_id: str = MISSING) -> None: + super().__init__(title=title, timeout=timeout, custom_id=custom_id) + self.category.default = category + self.link_title.default = link_title + self.link.default = link + self.link_id = link_id + + category = ui.TextInput(label='Kategorie') + link_title = ui.TextInput(label='Titel') + link = ui.TextInput(label='Link') + + def validate_link(self): + if not re.match("^https?://.+", self.link.value): + raise InvalidLinkError(f"`{self.link}` ist kein gültiger Link") + + async def on_submit(self, interaction: discord.Interaction): + self.validate_link() + db_category = models.LinkCategory.get_or_create(channel=interaction.channel_id, name=self.category) + + if self.link_id is None: + models.Link.create(link=self.link, title=self.link_title, category=db_category[0].id) + await interaction.response.send_message(content="Link erfolgreich hinzugefügt.", ephemeral=True) + else: + if link := models.Link.get_or_none(models.Link.id == self.link_id): + link_category = link.category + link.update(title=self.link_title, link=self.link, category=db_category[0].id).where( + models.Link.id == link.id).execute() + + if link_category.id != db_category[0].id and link.category.links.count() == 0: + link_category.delete_instance() + else: + raise LinkDoesNotExistError(f"Der Link `{self.link_title}` existiert nicht.") + + await interaction.response.send_message(content="Link erfolgreich bearbeitet.", ephemeral=True) + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + if type(error) in [InvalidLinkError, LinkDoesNotExistError]: + await interaction.response.send_message(content=error, ephemeral=True) + else: + await interaction.response.send_message(content="Fehler beim Hinzufügen/Bearbeiten eines Links.", + ephemeral=True) + traceback.print_exception(type(error), error, error.__traceback__) + + +class LinkCategoryModal(ui.Modal, title='Kategorie umbenennen'): + def __init__(self, *, db_category: str = None, link_id: int = None, + title: str = MISSING, timeout: Optional[float] = None, custom_id: str = MISSING) -> None: + super().__init__(title=title, timeout=timeout, custom_id=custom_id) + self.db_category = db_category + self.category.default = db_category.name + self.link_id = link_id + + category = ui.TextInput(label='Kategorie') + + async def on_submit(self, interaction: discord.Interaction): + self.db_category.update(name=self.category).where(models.LinkCategory.id == self.db_category.id).execute() + await interaction.response.send_message(content="Kategorie erfolgreich umbenannt.", ephemeral=True) + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + await interaction.response.send_message(content=f"Fehler beim umbenennen der Kategorie `{self.category}`.", + ephemeral=True) + traceback.print_exception(type(error), error, error.__traceback__) diff --git a/models.py b/models.py new file mode 100644 index 0000000..71393bc --- /dev/null +++ b/models.py @@ -0,0 +1,45 @@ +import discord +from peewee import * +from peewee import ModelSelect + +db = SqliteDatabase("db.sqlite3") + + +class BaseModel(Model): + class Meta: + database = db + legacy_table_names = False + + +class LinkCategory(BaseModel): + channel = IntegerField() + name = CharField() + + @classmethod + def get_categories(cls, channel: int, category: str = None) -> ModelSelect: + categories: ModelSelect = cls.select().where(LinkCategory.channel == channel) + return categories.where(LinkCategory.name == category) if category else categories + + @classmethod + def has_links(cls, channel: int, category: str = None) -> bool: + for category in cls.get_categories(channel, category=category): + if category.links.count() > 0: + return True + + return False + + def append_field(self, embed: discord.Embed) -> None: + value = "" + for link in self.links: + value += f"- [{link.title}]({link.link})\n" + + embed.add_field(name=self.name, value=value, inline=False) + + +class Link(BaseModel): + link = CharField() + title = CharField() + category = ForeignKeyField(LinkCategory, backref='links') + + +db.create_tables([LinkCategory, Link], safe=True) diff --git a/requirements.txt b/requirements.txt index cbc5aa1..bd6ab03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,11 +6,13 @@ beautifulsoup4==4.11.1 certifi==2022.12.7 cffi==1.15.1 charset-normalizer==2.1.1 -discord.py==2.1.0 +discord==2.2.3 +discord.py==2.2.3 emoji==2.2.0 frozenlist==1.3.3 idna==3.4 multidict==6.0.4 +peewee==3.16.2 pycparser==2.21 PyNaCl==1.5.0 python-dotenv==0.21.0 diff --git a/views/poll_view.py b/views/poll_view.py index d6494d7..390eb4e 100644 --- a/views/poll_view.py +++ b/views/poll_view.py @@ -15,7 +15,7 @@ async def show_participants(interaction, poll, ephemeral): choice_msg = f"{choices[idx][0]} {choices[idx][1]} ({choices[idx][2]}):" choice_msg += "<@" if choices[idx][2] > 0 else "" choice_msg += ">, <@".join(participants) - choice_msg += ">\n" if choices[idx][2] > 0 else "\n" + choice_msg += ">\n" if choices[idx][2] > 0 else "" if len(msg) + len(choice_msg) >= utils.MAX_MESSAGE_LEN: await interaction.followup.send(msg, ephemeral=ephemeral) msg = choice_msg @@ -110,7 +110,7 @@ class PollDropdown(discord.ui.Select): for idx, choice in enumerate(self.poll["choices"]): choice[2] = choices[idx] - await self.message.edit(embed=self.polls.get_embed(self.poll), view=PollView(self.polls)) + await self.message.edit(embed=self.polls.get_embed(self.poll), view=PollView(self.poll)) self.polls.save() def is_default(self, participant, idx): -- GitLab