This commit is contained in:
2025-12-19 20:40:02 +01:00
parent 045e8febaa
commit 34ef658b36
9 changed files with 257 additions and 245 deletions

4
.envtempl Normal file
View File

@@ -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

45
ComitsPaginatedView.py Normal file
View File

@@ -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

57
CommitsActionView.py Normal file
View File

@@ -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)

36
CommitsSelectMenu.py Normal file
View File

@@ -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)

32
FileBrowserSelect.py Normal file
View File

@@ -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)

55
PaginatedView.py Normal file
View File

@@ -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)

23
SearchModal.py Normal file
View File

@@ -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)

0
__init__.py Normal file
View File

250
main.py
View File

@@ -7,6 +7,11 @@ from datetime import datetime
from typing import List, Dict, Optional from typing import List, Dict, Optional
from dotenv import load_dotenv from dotenv import load_dotenv
from CommitsActionView import CommitActionsView
import SearchModal
import FileBrowserSelect
from ComitsPaginatedView import CommitsPaginatedView
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
@@ -35,249 +40,6 @@ bot = commands.Bot(
# Store last checked commit # Store last checked commit
last_commit_sha = None 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 ---------- # ---------- API FUNCTIONS ----------
async def fetch_commits(limit=10, sha=None): 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}") print(f"Exception fetching commits: {e}")
return None return None
async def fetch_commit_files(sha): async def fetch_commit_files(sha):
"""Fetch file changes for a specific commit""" """Fetch file changes for a specific commit"""
try: try:
@@ -331,7 +92,6 @@ async def fetch_commit_diff(sha):
print(f"Exception fetching diff: {e}") print(f"Exception fetching diff: {e}")
return None return None
def format_diff_stats(files): def format_diff_stats(files):
"""Format file changes into a readable string""" """Format file changes into a readable string"""
if not files: if not files: