From 34ef658b3683fe2a2352eda4f5cca6e60dd31f44 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Fri, 19 Dec 2025 20:40:02 +0100 Subject: [PATCH] cleanup --- .envtempl | 4 + ComitsPaginatedView.py | 45 ++++++++ CommitsActionView.py | 57 ++++++++++ CommitsSelectMenu.py | 36 ++++++ FileBrowserSelect.py | 32 ++++++ PaginatedView.py | 55 +++++++++ SearchModal.py | 23 ++++ __init__.py | 0 main.py | 250 +---------------------------------------- 9 files changed, 257 insertions(+), 245 deletions(-) create mode 100644 .envtempl create mode 100644 ComitsPaginatedView.py create mode 100644 CommitsActionView.py create mode 100644 CommitsSelectMenu.py create mode 100644 FileBrowserSelect.py create mode 100644 PaginatedView.py create mode 100644 SearchModal.py create mode 100644 __init__.py diff --git a/.envtempl b/.envtempl new file mode 100644 index 0000000..c7efa67 --- /dev/null +++ b/.envtempl @@ -0,0 +1,4 @@ +# This is a sample / template enviroment file +# It is relativley straight forward and you dont need too many things to get started +DISCORD_BOT_TOKEN= # Your API key for Discord, you get this in your bot console +DISCORD_CHANNEL_ID= # your channel where you want the bot to deposit its contents \ No newline at end of file diff --git a/ComitsPaginatedView.py b/ComitsPaginatedView.py new file mode 100644 index 0000000..edca02c --- /dev/null +++ b/ComitsPaginatedView.py @@ -0,0 +1,45 @@ +import PaginatedView +from CommitsSelectMenu import CommitSelectMenu +from typing import * +import datetime +from discord.ui import Select, SelectOption +import discord + +BRANCH = "main" + +class CommitsPaginatedView(PaginatedView): + """Paginated view for commits""" + def __init__(self, commits: List[Dict], repo_info: str): + super().__init__(commits, page_size=5) + self.repo_info = repo_info + self.commits = commits + # Add file selection button + self.add_item(CommitSelectMenu(self.commits)) + + async def update_message(self, interaction: discord.Interaction): + embed = self.create_embed() + await interaction.response.edit_message(embed=embed, view=self) + + def create_embed(self): + embed = discord.Embed( + title=f"šŸ“š Commits ({self.current_page + 1}/{self.total_pages})", + description=f"**Repository:** {self.repo_info}\n**Branch:** {BRANCH}", + color=discord.Color.blue() + ) + + page_data = self.get_page_data() + for i, commit in enumerate(page_data, start=self.current_page * self.page_size): + msg = commit['commit']['message'].split('\n')[0][:80] + author = commit['commit']['author']['name'] + sha = commit['sha'][:7] + date = datetime.fromisoformat(commit['commit']['committer']['date'].replace('Z', '+00:00')) + + embed.add_field( + name=f"{i+1}. `{sha}` by {author}", + value=f"```{msg}```\nšŸ•’ {discord.utils.format_dt(date, 'R')}", + inline=False + ) + + embed.set_footer(text="Select a commit from the dropdown below to view details") + return embed + diff --git a/CommitsActionView.py b/CommitsActionView.py new file mode 100644 index 0000000..e3b6116 --- /dev/null +++ b/CommitsActionView.py @@ -0,0 +1,57 @@ +from typing import Dict, List +import discord +from discord.ui import View, Button +from FileBrowserSelect import FileBrowserSelect + +class CommitActionsView(View): + """Action buttons for a specific commit""" + def __init__(self, commit: Dict, files: List[Dict], timeout: int = 60): + super().__init__(timeout=timeout) + self.commit = commit + self.files = files + self.commit_sha = commit['sha'] + self.commit_short = commit['sha'][:7] + + # Link button that opens the commit in Gitea + link_button = Button( + label="šŸ”— Open in Gitea", + style=discord.ButtonStyle.link, + url=commit['html_url'], + emoji="šŸ”—" + ) + self.add_item(link_button) + + # Add a file browser select if files exist + if files: + self.add_item(FileBrowserSelect(files, self.commit_sha)) + + @discord.ui.button(label="šŸ“Š View Diff", style=discord.ButtonStyle.primary) + async def view_diff(self, interaction: discord.Interaction, button: Button): + # Import here to avoid circular import with main.py + from main import show_commit_diff_interactive + await show_commit_diff_interactive(interaction, self.commit_sha) + + @discord.ui.button(label="šŸ“ Browse Files", style=discord.ButtonStyle.secondary) + async def browse_files(self, interaction: discord.Interaction, button: Button): + if not self.files: + await interaction.response.send_message("No files changed in this commit.", ephemeral=True) + return + + embed = discord.Embed( + title=f"šŸ“ Files in commit `{self.commit_short}`", + description="Select a file to view its diff:", + color=discord.Color.blue() + ) + + file_list = "\n".join([ + f"• `{f['filename']}` (+{f.get('additions', 0)}/-{f.get('deletions', 0)})" + for f in self.files[:10] + ]) + if len(self.files) > 10: + file_list += f"\n... and {len(self.files) - 10} more" + + embed.add_field(name="Changed Files", value=file_list, inline=False) + + view = FileBrowserSelect(self.files, self.commit_sha) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + diff --git a/CommitsSelectMenu.py b/CommitsSelectMenu.py new file mode 100644 index 0000000..f8e81e2 --- /dev/null +++ b/CommitsSelectMenu.py @@ -0,0 +1,36 @@ +import discord +from typing import * +from discord.ui import * + +from main import create_commit_embed, fetch_commit_files +from CommitsActionView import CommitActionsView + +class CommitSelectMenu(Select): + """Dropdown menu for selecting a commit""" + def __init__(self, commits: List[Dict]): + options = [ + discord.SelectOption( + label=f"{commit['sha'][:7]} - {commit['commit']['message'].split('\n')[0][:45]}", + value=str(i), + description=f"by {commit['commit']['author']['name']}" + ) + for i, commit in enumerate(commits[:25]) # Discord limit: 25 options + ] + super().__init__( + placeholder="šŸ“ Select a commit to view details...", + min_values=1, + max_values=1, + options=options + ) + self.commits = commits + + async def callback(self, interaction: discord.Interaction): + selected_idx = int(self.values[0]) + commit = self.commits[selected_idx] + + # Fetch file info + files = await fetch_commit_files(commit['sha']) + + embed = create_commit_embed(commit, files) + view = CommitActionsView(commit, files) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) \ No newline at end of file diff --git a/FileBrowserSelect.py b/FileBrowserSelect.py new file mode 100644 index 0000000..ddf235b --- /dev/null +++ b/FileBrowserSelect.py @@ -0,0 +1,32 @@ +from discord.ui import * +import discord +from typing import * +from main import show_file_diff + +class FileBrowserSelect(Select): + """Dropdown for browsing files in a commit""" + def __init__(self, files: List[Dict], commit_sha: str): + options = [ + discord.SelectOption( + label=f"{f['filename'][:45]}", + value=str(i), + description=f"+{f.get('additions', 0)}/-{f.get('deletions', 0)} - {f.get('status', 'modified')}" + ) + for i, f in enumerate(files[:25]) + ] + super().__init__( + placeholder="šŸ“„ Select a file to view diff...", + min_values=1, + max_values=1, + options=options + ) + self.files = files + self.commit_sha = commit_sha + + async def callback(self, interaction: discord.Interaction): + selected_idx = int(self.values[0]) + file_info = self.files[selected_idx] + filename = file_info['filename'] + + await show_file_diff(interaction, self.commit_sha, filename) + diff --git a/PaginatedView.py b/PaginatedView.py new file mode 100644 index 0000000..e249c4c --- /dev/null +++ b/PaginatedView.py @@ -0,0 +1,55 @@ +import discord +from discord.ui import View, Button +from typing import List + +class PaginatedView(View): + """Base class for paginated views""" + def __init__(self, data: List, page_size: int = 5, timeout: int = 60): + super().__init__(timeout=timeout) + self.data = data + self.page_size = page_size + self.current_page = 0 + self.total_pages = (len(data) + page_size - 1) // page_size + self.update_buttons() + + def update_buttons(self): + """Enable/disable navigation buttons based on current page""" + self.children[0].disabled = self.current_page == 0 # First + self.children[1].disabled = self.current_page == 0 # Previous + self.children[2].disabled = self.current_page >= self.total_pages - 1 # Next + self.children[3].disabled = self.current_page >= self.total_pages - 1 # Last + + def get_page_data(self): + """Get data for current page""" + start = self.current_page * self.page_size + end = start + self.page_size + return self.data[start:end] + + async def update_message(self, interaction: discord.Interaction): + """Update the message with current page""" + pass + + @discord.ui.button(emoji="āŖ", style=discord.ButtonStyle.secondary) + async def first_page(self, interaction: discord.Interaction, button: Button): + self.current_page = 0 + self.update_buttons() + await self.update_message(interaction) + + @discord.ui.button(emoji="ā—€ļø", style=discord.ButtonStyle.secondary) + async def previous_page(self, interaction: discord.Interaction, button: Button): + self.current_page = max(0, self.current_page - 1) + self.update_buttons() + await self.update_message(interaction) + + @discord.ui.button(emoji="ā–¶ļø", style=discord.ButtonStyle.secondary) + async def next_page(self, interaction: discord.Interaction, button: Button): + self.current_page = min(self.total_pages - 1, self.current_page + 1) + self.update_buttons() + await self.update_message(interaction) + + @discord.ui.button(emoji="ā©", style=discord.ButtonStyle.secondary) + async def last_page(self, interaction: discord.Interaction, button: Button): + self.current_page = self.total_pages - 1 + self.update_buttons() + await self.update_message(interaction) + diff --git a/SearchModal.py b/SearchModal.py new file mode 100644 index 0000000..267d8ae --- /dev/null +++ b/SearchModal.py @@ -0,0 +1,23 @@ +from discord.ui import * +import discord + +from main import search_commits + +class SearchModal(Modal): + """Modal for searching commits""" + def __init__(self, search_type: str = "message"): + super().__init__(title=f"šŸ” Search Commits by {search_type.capitalize()}") + self.search_type = search_type + + self.search_term = TextInput( + label=f"Enter search term:", + placeholder=f"Search in commit {search_type}...", + min_length=2, + max_length=100 + ) + self.add_item(self.search_term) + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer() + await search_commits(interaction, self.search_type, self.search_term.value) + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py index 9db8351..2d65561 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,11 @@ from datetime import datetime from typing import List, Dict, Optional from dotenv import load_dotenv +from CommitsActionView import CommitActionsView +import SearchModal +import FileBrowserSelect +from ComitsPaginatedView import CommitsPaginatedView + # Load environment variables from .env file load_dotenv() @@ -35,249 +40,6 @@ bot = commands.Bot( # Store last checked commit last_commit_sha = None -# ---------- UI COMPONENTS ---------- - -class PaginatedView(View): - """Base class for paginated views""" - def __init__(self, data: List, page_size: int = 5, timeout: int = 60): - super().__init__(timeout=timeout) - self.data = data - self.page_size = page_size - self.current_page = 0 - self.total_pages = (len(data) + page_size - 1) // page_size - self.update_buttons() - - def update_buttons(self): - """Enable/disable navigation buttons based on current page""" - self.children[0].disabled = self.current_page == 0 # First - self.children[1].disabled = self.current_page == 0 # Previous - self.children[2].disabled = self.current_page >= self.total_pages - 1 # Next - self.children[3].disabled = self.current_page >= self.total_pages - 1 # Last - - def get_page_data(self): - """Get data for current page""" - start = self.current_page * self.page_size - end = start + self.page_size - return self.data[start:end] - - async def update_message(self, interaction: discord.Interaction): - """Update the message with current page""" - pass - - @discord.ui.button(emoji="āŖ", style=discord.ButtonStyle.secondary) - async def first_page(self, interaction: discord.Interaction, button: Button): - self.current_page = 0 - self.update_buttons() - await self.update_message(interaction) - - @discord.ui.button(emoji="ā—€ļø", style=discord.ButtonStyle.secondary) - async def previous_page(self, interaction: discord.Interaction, button: Button): - self.current_page = max(0, self.current_page - 1) - self.update_buttons() - await self.update_message(interaction) - - @discord.ui.button(emoji="ā–¶ļø", style=discord.ButtonStyle.secondary) - async def next_page(self, interaction: discord.Interaction, button: Button): - self.current_page = min(self.total_pages - 1, self.current_page + 1) - self.update_buttons() - await self.update_message(interaction) - - @discord.ui.button(emoji="ā©", style=discord.ButtonStyle.secondary) - async def last_page(self, interaction: discord.Interaction, button: Button): - self.current_page = self.total_pages - 1 - self.update_buttons() - await self.update_message(interaction) - - -class CommitsPaginatedView(PaginatedView): - """Paginated view for commits""" - def __init__(self, commits: List[Dict], repo_info: str): - super().__init__(commits, page_size=5) - self.repo_info = repo_info - self.commits = commits - # Add file selection button - self.add_item(CommitSelectMenu(self.commits)) - - async def update_message(self, interaction: discord.Interaction): - embed = self.create_embed() - await interaction.response.edit_message(embed=embed, view=self) - - def create_embed(self): - embed = discord.Embed( - title=f"šŸ“š Commits ({self.current_page + 1}/{self.total_pages})", - description=f"**Repository:** {self.repo_info}\n**Branch:** {BRANCH}", - color=discord.Color.blue() - ) - - page_data = self.get_page_data() - for i, commit in enumerate(page_data, start=self.current_page * self.page_size): - msg = commit['commit']['message'].split('\n')[0][:80] - author = commit['commit']['author']['name'] - sha = commit['sha'][:7] - date = datetime.fromisoformat(commit['commit']['committer']['date'].replace('Z', '+00:00')) - - embed.add_field( - name=f"{i+1}. `{sha}` by {author}", - value=f"```{msg}```\nšŸ•’ {discord.utils.format_dt(date, 'R')}", - inline=False - ) - - embed.set_footer(text="Select a commit from the dropdown below to view details") - return embed - - -class CommitSelectMenu(Select): - """Dropdown menu for selecting a commit""" - def __init__(self, commits: List[Dict]): - options = [ - discord.SelectOption( - label=f"{commit['sha'][:7]} - {commit['commit']['message'].split('\n')[0][:45]}", - value=str(i), - description=f"by {commit['commit']['author']['name']}" - ) - for i, commit in enumerate(commits[:25]) # Discord limit: 25 options - ] - super().__init__( - placeholder="šŸ“ Select a commit to view details...", - min_values=1, - max_values=1, - options=options - ) - self.commits = commits - - async def callback(self, interaction: discord.Interaction): - selected_idx = int(self.values[0]) - commit = self.commits[selected_idx] - - # Fetch file info - files = await fetch_commit_files(commit['sha']) - - embed = create_commit_embed(commit, files) - view = CommitActionsView(commit, files) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - - -class CommitActionsView(View): - """Action buttons for a specific commit""" - def __init__(self, commit: Dict, files: List[Dict], timeout: int = 60): - super().__init__(timeout=timeout) - self.commit = commit - self.files = files - self.commit_sha = commit['sha'] - self.commit_short = commit['sha'][:7] - - # 1. Regular buttons with callbacks - view_diff_button = Button( - label="šŸ“Š View Diff", - style=discord.ButtonStyle.primary, - custom_id=f"view_diff_{self.commit_short}" - ) - view_diff_button.callback = self.view_diff_callback - self.add_item(view_diff_button) - - browse_files_button = Button( - label="šŸ“ Browse Files", - style=discord.ButtonStyle.secondary, - custom_id=f"browse_files_{self.commit_short}" - ) - browse_files_button.callback = self.browse_files_callback - self.add_item(browse_files_button) - - # 2. Link button - MUST have a URL, not a callback - if 'html_url' in commit and commit['html_url']: - link_button = Button( - label="šŸ”— Open in Gitea", - style=discord.ButtonStyle.link, - url=commit['html_url'] # Required for link buttons - ) - self.add_item(link_button) - else: - # If no URL, disable the button - disabled_button = Button( - label="šŸ”— No Link Available", - style=discord.ButtonStyle.secondary, - disabled=True - ) - self.add_item(disabled_button) - - # 3. Add file browser dropdown if files exist - if files: - self.add_item(FileBrowserSelect(files, self.commit_sha)) - - async def view_diff_callback(self, interaction: discord.Interaction): - """Callback for viewing diff""" - await interaction.response.defer(ephemeral=True) - await show_commit_diff_interactive(interaction, self.commit_sha) - - async def browse_files_callback(self, interaction: discord.Interaction): - """Callback for browsing files""" - if not self.files: - await interaction.response.send_message("No files changed in this commit.", ephemeral=True) - return - - embed = discord.Embed( - title=f"šŸ“ Files in commit `{self.commit_short}`", - description="Select a file to view its diff:", - color=discord.Color.blue() - ) - - file_list = "\n".join([f"• `{f['filename']}` (+{f.get('additions', 0)}/-{f.get('deletions', 0)})" - for f in self.files[:10]]) - if len(self.files) > 10: - file_list += f"\n... and {len(self.files) - 10} more" - - embed.add_field(name="Changed Files", value=file_list, inline=False) - - view = FileBrowserSelect(self.files, self.commit_sha) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - -class FileBrowserSelect(Select): - """Dropdown for browsing files in a commit""" - def __init__(self, files: List[Dict], commit_sha: str): - options = [ - discord.SelectOption( - label=f"{f['filename'][:45]}", - value=str(i), - description=f"+{f.get('additions', 0)}/-{f.get('deletions', 0)} - {f.get('status', 'modified')}" - ) - for i, f in enumerate(files[:25]) - ] - super().__init__( - placeholder="šŸ“„ Select a file to view diff...", - min_values=1, - max_values=1, - options=options - ) - self.files = files - self.commit_sha = commit_sha - - async def callback(self, interaction: discord.Interaction): - selected_idx = int(self.values[0]) - file_info = self.files[selected_idx] - filename = file_info['filename'] - - await show_file_diff(interaction, self.commit_sha, filename) - - -class SearchModal(Modal): - """Modal for searching commits""" - def __init__(self, search_type: str = "message"): - super().__init__(title=f"šŸ” Search Commits by {search_type.capitalize()}") - self.search_type = search_type - - self.search_term = TextInput( - label=f"Enter search term:", - placeholder=f"Search in commit {search_type}...", - min_length=2, - max_length=100 - ) - self.add_item(self.search_term) - - async def on_submit(self, interaction: discord.Interaction): - await interaction.response.defer() - await search_commits(interaction, self.search_type, self.search_term.value) - - # ---------- API FUNCTIONS ---------- async def fetch_commits(limit=10, sha=None): @@ -297,7 +59,6 @@ async def fetch_commits(limit=10, sha=None): print(f"Exception fetching commits: {e}") return None - async def fetch_commit_files(sha): """Fetch file changes for a specific commit""" try: @@ -331,7 +92,6 @@ async def fetch_commit_diff(sha): print(f"Exception fetching diff: {e}") return None - def format_diff_stats(files): """Format file changes into a readable string""" if not files: