massive fixes

This commit is contained in:
seppmutterman@gmail.com
2025-12-19 21:11:27 +01:00
parent 34ef658b36
commit 7d27be69f8
9 changed files with 24 additions and 10 deletions

View File

@@ -0,0 +1,46 @@
from PaginatedView import PaginatedView
from CommitsSelectMenu import CommitSelectMenu
from typing import *
import datetime
from discord.ui import Select
from discord import 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.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
src/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
src/CommitsSelectMenu.py Normal file
View File

@@ -0,0 +1,36 @@
import discord
from typing import *
from discord.ui import *
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):
from main import create_commit_embed, fetch_commit_files
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
src/FileBrowserSelect.py Normal file
View File

@@ -0,0 +1,32 @@
from discord.ui import *
import discord
from typing import *
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']
from main import show_file_diff
await show_file_diff(interaction, self.commit_sha, filename)

55
src/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)

22
src/SearchModal.py Normal file
View File

@@ -0,0 +1,22 @@
from discord.ui import *
import discord
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):
from main import search_commits
await interaction.response.defer()
await search_commits(interaction, self.search_type, self.search_term.value)

853
src/main.py Normal file
View File

@@ -0,0 +1,853 @@
import discord
from discord.ext import commands, tasks
from discord.ui import Button, View, Select, Modal, TextInput
import aiohttp
import os
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()
# Configuration
DISCORD_TOKEN = os.getenv('DISCORD_BOT_TOKEN', '').strip()
CHANNEL_ID = int(os.getenv('DISCORD_CHANNEL_ID', '0'))
GITEA_BASE_URL = 'http://rattatwinko.servecounterstrike.com/gitea'
GITEA_API_BASE = f'{GITEA_BASE_URL}/api/v1'
REPO_OWNER = 'rattatwinko'
REPO_NAME = 'INF6B'
BRANCH = 'main'
CHECK_INTERVAL = 60 # seconds
# Bot setup
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
intents.members = True # For better interactions
bot = commands.Bot(
command_prefix='!',
intents=intents,
help_command=None # We'll create custom help
)
# Store last checked commit
last_commit_sha = None
# ---------- API FUNCTIONS ----------
async def fetch_commits(limit=10, sha=None):
"""Fetch recent commits from Gitea API"""
try:
url = f'{GITEA_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/commits'
params = {'sha': sha or BRANCH, 'limit': limit}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as resp:
if resp.status == 200:
return await resp.json()
else:
print(f"Error fetching commits: {resp.status}")
return None
except Exception as e:
print(f"Exception fetching commits: {e}")
return None
async def fetch_commit_files(sha):
"""Fetch file changes for a specific commit"""
try:
url = f'{GITEA_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/git/commits/{sha}'
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 200:
data = await resp.json()
return data.get('files', [])
else:
return None
except Exception as e:
print(f"Exception fetching commit files: {e}")
return None
async def fetch_commit_diff(sha):
"""Fetch the actual diff text for a commit"""
try:
url = f'{GITEA_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/git/commits/{sha}.diff'
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 200:
try:
return await resp.text(encoding='utf-8')
except UnicodeDecodeError:
return await resp.text(encoding='latin-1')
else:
return None
except Exception as e:
print(f"Exception fetching diff: {e}")
return None
def format_diff_stats(files):
"""Format file changes into a readable string"""
if not files:
return "No file changes available", 0, 0
stats = []
total_additions = 0
total_deletions = 0
for file in files[:10]:
filename = file.get('filename', 'Unknown')
additions = file.get('additions', 0)
deletions = file.get('deletions', 0)
status = file.get('status', 'modified')
total_additions += additions
total_deletions += deletions
# Create a visual indicator
if status == 'added':
icon = '🆕'
elif status == 'removed':
icon = '🗑️'
elif status == 'renamed':
icon = '📝'
else:
icon = '📄'
stats.append(f"{icon} `{filename}` (+{additions}/-{deletions})")
if len(files) > 10:
stats.append(f"... and {len(files) - 10} more files")
return "\n".join(stats), total_additions, total_deletions
def create_commit_embed(commit, files=None):
"""Create a Discord embed for a commit"""
commit_msg = commit['commit']['message']
msg_lines = commit_msg.split('\n', 1)
title = msg_lines[0][:256]
description = msg_lines[1][:500] if len(msg_lines) > 1 else ""
embed = discord.Embed(
title=f"📝 Commit: {title}",
description=description if description else None,
color=discord.Color.green(),
timestamp=datetime.fromisoformat(commit['commit']['committer']['date'].replace('Z', '+00:00')),
url=commit['html_url']
)
embed.set_author(
name=commit['commit']['author']['name'],
icon_url=commit['author']['avatar_url'] if commit.get('author') else None
)
embed.add_field(name="👤 Author", value=commit['commit']['author']['name'], inline=True)
embed.add_field(name="🔑 Commit Hash", value=f"`{commit['sha'][:7]}`", inline=True)
embed.add_field(name="🌿 Branch", value=BRANCH, inline=True)
if files:
diff_text, additions, deletions = format_diff_stats(files)
embed.add_field(
name=f"📊 File Changes (+{additions}/-{deletions})",
value=diff_text,
inline=False
)
embed.set_footer(text=f"{REPO_OWNER}/{REPO_NAME} • Use buttons below to explore")
return embed
# ---------- INTERACTIVE FUNCTIONS ----------
async def show_commit_diff_interactive(interaction: discord.Interaction, commit_hash: str):
"""Show diff with interactive controls"""
try:
# Find the commit
commits = await fetch_commits(50)
matching_commit = None
for commit in commits:
if commit['sha'].startswith(commit_hash):
matching_commit = commit
break
if not matching_commit:
await interaction.followup.send("Commit not found.", ephemeral=True)
return
diff_text = await fetch_commit_diff(matching_commit['sha'])
if not diff_text:
await interaction.followup.send("Could not fetch diff.", ephemeral=True)
return
# Split diff into manageable chunks
lines = diff_text.split('\n')
chunks = []
current_chunk = []
current_length = 0
for line in lines:
line_length = len(line) + 1
if current_length + line_length > 1800 or len(current_chunk) >= 40:
if current_chunk:
chunks.append('\n'.join(current_chunk))
current_chunk = [line]
current_length = line_length
else:
current_chunk.append(line)
current_length += line_length
if current_chunk:
chunks.append('\n'.join(current_chunk))
# Create paginated view for diff
if len(chunks) > 1:
embed = discord.Embed(
title=f"📊 Diff for commit `{matching_commit['sha'][:7]}`",
description=f"**Part 1/{len(chunks)}**",
color=discord.Color.blue()
)
await interaction.followup.send(embed=embed, ephemeral=True)
for i, chunk in enumerate(chunks, 1):
await interaction.followup.send(f"```diff\n{chunk}\n```", ephemeral=True)
else:
embed = discord.Embed(
title=f"📊 Diff for commit `{matching_commit['sha'][:7]}`",
color=discord.Color.blue()
)
try:
await interaction.followup.send(embed=embed, ephemeral=True)
await interaction.followup.send(f"```diff\n{diff_text[:1800]}\n```", ephemeral=True)
except Exception as e:
print(f"err at main.py with {e}")
except Exception as e:
await interaction.followup.send(f"Error: {str(e)}", ephemeral=True)
async def show_file_diff(interaction: discord.Interaction, commit_sha: str, filename: str):
"""Show diff for a specific file"""
try:
# Fetch full diff
full_diff = await fetch_commit_diff(commit_sha)
if not full_diff:
await interaction.response.send_message("Could not fetch diff.", ephemeral=True)
return
# Extract file diff
lines = full_diff.split('\n')
file_diff = []
in_target_file = False
for line in lines:
if line.startswith('diff --git'):
if f'b/{filename}' in line or f'a/{filename}' in line:
in_target_file = True
file_diff.append(line)
elif in_target_file:
break
elif in_target_file:
file_diff.append(line)
if not file_diff:
await interaction.response.send_message(f"No diff found for `{filename}`", ephemeral=True)
return
diff_text = '\n'.join(file_diff)
# Create embed with file info
embed = discord.Embed(
title=f"📄 {filename}",
description=f"**Commit:** `{commit_sha[:7]}`",
color=discord.Color.blue()
)
if len(diff_text) > 1800:
embed.add_field(name="Diff Preview", value=f"```diff\n{diff_text[:500]}...\n```", inline=False)
embed.set_footer(text="Diff too large to display fully")
await interaction.response.send_message(embed=embed, ephemeral=True)
# Send the rest in follow-up
chunks = [diff_text[i:i+1800] for i in range(0, len(diff_text), 1800)]
for i, chunk in enumerate(chunks[1:], 2):
await interaction.followup.send(f"```diff\n{chunk}\n```", ephemeral=True)
else:
embed.add_field(name="Changes", value=f"```diff\n{diff_text}\n```", inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
except Exception as e:
await interaction.response.send_message(f"Error: {str(e)}", ephemeral=True)
async def search_commits(interaction: discord.Interaction, search_type: str, search_term: str):
"""Search commits by various criteria"""
try:
commits = await fetch_commits(100) # Get more commits for searching
if not commits:
await interaction.followup.send("Could not fetch commits.", ephemeral=True)
return
matching_commits = []
search_term_lower = search_term.lower()
for commit in commits:
if search_type == "message":
if search_term_lower in commit['commit']['message'].lower():
matching_commits.append(commit)
elif search_type == "author":
if search_term_lower in commit['commit']['author']['name'].lower():
matching_commits.append(commit)
elif search_type == "hash":
if commit['sha'].startswith(search_term_lower):
matching_commits.append(commit)
if not matching_commits:
await interaction.followup.send(f"No commits found matching '{search_term}'", ephemeral=True)
return
embed = discord.Embed(
title=f"🔍 Search Results: {len(matching_commits)} commits found",
description=f"Searching by {search_type} for '{search_term}'",
color=discord.Color.purple()
)
for i, commit in enumerate(matching_commits[:5]):
msg = commit['commit']['message'].split('\n')[0][:80]
author = commit['commit']['author']['name']
sha = commit['sha'][:7]
embed.add_field(
name=f"{i+1}. `{sha}` by {author}",
value=f"```{msg}```\n`!commit view {sha}`",
inline=False
)
if len(matching_commits) > 5:
embed.add_field(
name="More Results",
value=f"... and {len(matching_commits) - 5} more commits\nUse `!commit search` for more specific searches",
inline=False
)
await interaction.followup.send(embed=embed, ephemeral=True)
except Exception as e:
await interaction.followup.send(f"Error: {str(e)}", ephemeral=True)
# ---------- TASK ----------
@tasks.loop(seconds=CHECK_INTERVAL)
async def check_commits():
"""Periodically check for new commits"""
global last_commit_sha
commits = await fetch_commits()
if not commits or len(commits) == 0:
return
latest_commit = commits[0]
latest_sha = latest_commit['sha']
if last_commit_sha is None:
last_commit_sha = latest_sha
return
if latest_sha != last_commit_sha:
print(f"New commit detected: {latest_sha[:7]}")
new_commits = []
for commit in commits:
if commit['sha'] == last_commit_sha:
break
new_commits.append(commit)
channel = bot.get_channel(CHANNEL_ID)
if channel:
for commit in reversed(new_commits):
files = await fetch_commit_files(commit['sha'])
embed = create_commit_embed(commit, files)
# Add action buttons for new commits
view = CommitActionsView(commit, files)
try:
await channel.send(embed=embed, view=view)
except Exception as e:
print(f"Error sending notification: {e}")
last_commit_sha = latest_sha
# ---------- INTUITIVE COMMANDS ----------
@bot.command(name="help")
async def help_command(ctx):
"""Show interactive help menu"""
embed = discord.Embed(
title="🤖 Gitea Monitor Bot Help",
description=f"Monitoring **{REPO_OWNER}/{REPO_NAME}** on branch **{BRANCH}**\n\n"
"**📋 Quick Commands:**",
color=discord.Color.blue()
)
# Main commands
embed.add_field(
name="🔍 `!commits`",
value="Browse recent commits with pagination",
inline=False
)
embed.add_field(
name="📝 `!commit view <hash>`",
value="View details of a specific commit\nExample: `!commit view abc123`",
inline=False
)
embed.add_field(
name="🔎 `!commit search`",
value="Search commits by message, author, or hash",
inline=False
)
embed.add_field(
name="📊 `!commit diff <hash>`",
value="Show diff for a commit\nExample: `!commit diff abc123`",
inline=False
)
embed.add_field(
name="📁 `!files <hash>`",
value="Browse files changed in a commit\nExample: `!files abc123`",
inline=False
)
embed.add_field(
name="📈 `!status`",
value="Show bot status and latest commit",
inline=False
)
embed.add_field(
name="⚙️ `!monitor`",
value="Monitor a different branch or repository\n*(Admin only)*",
inline=False
)
# Quick actions view
view = View(timeout=60)
view.add_item(Button(
label="📚 Browse Commits",
style=discord.ButtonStyle.primary,
custom_id="quick_browse"
))
view.add_item(Button(
label="🔍 Search Commits",
style=discord.ButtonStyle.secondary,
custom_id="quick_search"
))
view.add_item(Button(
label="📊 View Status",
style=discord.ButtonStyle.success,
custom_id="quick_status"
))
# Button callbacks
async def browse_callback(interaction: discord.Interaction):
await ctx.invoke(bot.get_command('commits'))
async def search_callback(interaction: discord.Interaction):
modal = SearchModal("message")
await interaction.response.send_modal(modal)
async def status_callback(interaction: discord.Interaction):
await ctx.invoke(bot.get_command('status'))
# Assign callbacks
for child in view.children:
if child.custom_id == "quick_browse":
child.callback = browse_callback
elif child.custom_id == "quick_search":
child.callback = search_callback
elif child.custom_id == "quick_status":
child.callback = status_callback
await ctx.send(embed=embed, view=view)
@bot.group(name="commit", invoke_without_command=True)
async def commit_group(ctx):
"""Main commit command group"""
if ctx.invoked_subcommand is None:
embed = discord.Embed(
title="📝 Commit Commands",
description="Available subcommands:",
color=discord.Color.blue()
)
embed.add_field(
name="`!commit view <hash>`",
value="View a specific commit\nExample: `!commit view abc123`",
inline=False
)
embed.add_field(
name="`!commit browse`",
value="Browse recent commits interactively",
inline=False
)
embed.add_field(
name="`!commit search`",
value="Search for commits",
inline=False
)
embed.add_field(
name="`!commit diff <hash>`",
value="Show diff for a commit",
inline=False
)
embed.add_field(
name="`!commit latest [count]`",
value="Show latest commits (default: 10)",
inline=False
)
await ctx.send(embed=embed)
@commit_group.command(name="view")
async def commit_view(ctx, commit_hash: str):
"""View details of a specific commit"""
try:
commits = await fetch_commits(50)
matching_commit = None
for commit in commits:
if commit['sha'].startswith(commit_hash):
matching_commit = commit
break
if not matching_commit:
await ctx.send(f"❌ Commit `{commit_hash}` not found.")
return
files = await fetch_commit_files(matching_commit['sha'])
embed = create_commit_embed(matching_commit, files)
view = CommitActionsView(matching_commit, files)
await ctx.send(embed=embed, view=view)
except Exception as e:
await ctx.send(f"❌ Error: {str(e)}")
@commit_group.command(name="browse")
async def commit_browse(ctx, count: int = 20):
"""Browse recent commits interactively"""
try:
if count < 1 or count > 50:
await ctx.send("❌ Please choose a count between 1 and 50.")
return
await ctx.send(f"🔍 Fetching {count} recent commits...")
commits = await fetch_commits(count)
if not commits:
await ctx.send("❌ Could not fetch commits.")
return
view = CommitsPaginatedView(commits, f"{REPO_OWNER}/{REPO_NAME}")
embed = view.create_embed()
await ctx.send(embed=embed, view=view)
except Exception as e:
await ctx.send(f"❌ Error: {str(e)}")
@commit_group.command(name="search")
async def commit_search(ctx):
"""Search commits interactively"""
view = View(timeout=60)
async def search_by_message(interaction: discord.Interaction):
modal = SearchModal("message")
await interaction.response.send_modal(modal)
async def search_by_author(interaction: discord.Interaction):
modal = SearchModal("author")
await interaction.response.send_modal(modal)
async def search_by_hash(interaction: discord.Interaction):
modal = SearchModal("hash")
await interaction.response.send_modal(modal)
# Create buttons
message_btn = Button(label="📝 Search by Message", style=discord.ButtonStyle.primary, emoji="📝")
author_btn = Button(label="👤 Search by Author", style=discord.ButtonStyle.primary, emoji="👤")
hash_btn = Button(label="🔑 Search by Hash", style=discord.ButtonStyle.primary, emoji="🔑")
message_btn.callback = search_by_message
author_btn.callback = search_by_author
hash_btn.callback = search_by_hash
view.add_item(message_btn)
view.add_item(author_btn)
view.add_item(hash_btn)
embed = discord.Embed(
title="🔍 Search Commits",
description="Choose how you want to search:",
color=discord.Color.purple()
)
embed.add_field(name="📝 By Message", value="Search in commit messages", inline=True)
embed.add_field(name="👤 By Author", value="Search by author name", inline=True)
embed.add_field(name="🔑 By Hash", value="Search by commit hash (partial)", inline=True)
await ctx.send(embed=embed, view=view)
@commit_group.command(name="diff")
async def commit_diff(ctx, commit_hash: str):
"""Show diff for a commit"""
await show_commit_diff_interactive(ctx.interaction if hasattr(ctx, 'interaction') else None, commit_hash)
@commit_group.command(name="latest")
async def commit_latest(ctx, count: int = 10):
"""Show latest commits"""
await commit_browse(ctx, count)
@bot.command(name="commits")
async def commits_browse(ctx, count: int = 20):
"""Browse commits (alias for !commit browse)"""
await commit_browse(ctx, count)
@bot.command(name="files")
async def files_browse(ctx, commit_hash: str):
"""Browse files changed in a commit"""
try:
commits = await fetch_commits(50)
matching_commit = None
for commit in commits:
if commit['sha'].startswith(commit_hash):
matching_commit = commit
break
if not matching_commit:
await ctx.send(f"❌ Commit `{commit_hash}` not found.")
return
files = await fetch_commit_files(matching_commit['sha'])
if not files:
await ctx.send("❌ No files changed in this commit.")
return
view = FileBrowserSelect(files, matching_commit['sha'])
embed = discord.Embed(
title=f"📁 Files in commit `{matching_commit['sha'][:7]}`",
description=f"**{len(files)}** files changed\n"
f"Select a file from the dropdown below:",
color=discord.Color.blue()
)
file_preview = "\n".join([f"• `{f['filename']}`" for f in files[:5]])
if len(files) > 5:
file_preview += f"\n... and {len(files) - 5} more"
embed.add_field(name="Files Changed", value=file_preview, inline=False)
await ctx.send(embed=embed, view=view)
except Exception as e:
await ctx.send(f"❌ Error: {str(e)}")
@bot.command(name="status")
async def bot_status(ctx):
"""Show bot status and repository info"""
global last_commit_sha
commits = await fetch_commits(1)
latest_commit = commits[0] if commits else None
embed = discord.Embed(
title="🤖 Bot Status Dashboard",
color=discord.Color.green()
)
# Repository info
embed.add_field(
name="📦 Repository",
value=f"[{REPO_OWNER}/{REPO_NAME}]({GITEA_BASE_URL}/{REPO_OWNER}/{REPO_NAME})",
inline=False
)
# Branch and monitoring
embed.add_field(name="🌿 Branch", value=BRANCH, inline=True)
embed.add_field(name="⏰ Check Interval", value=f"{CHECK_INTERVAL}s", inline=True)
embed.add_field(
name="✅ Monitoring",
value="🟢 Active" if check_commits.is_running() else "🔴 Inactive",
inline=True
)
# Last commit info
if last_commit_sha:
embed.add_field(name="📝 Last Known Commit", value=f"`{last_commit_sha[:7]}`", inline=True)
if latest_commit:
msg = latest_commit['commit']['message'].split('\n')[0][:100]
embed.add_field(
name="🆕 Latest Commit",
value=f"[`{latest_commit['sha'][:7]}`]({latest_commit['html_url']})\n```{msg}```",
inline=False
)
# Quick actions
embed.add_field(
name="🚀 Quick Actions",
value="• `!commits` - Browse recent commits\n"
"• `!commit view <hash>` - View a specific commit\n"
"• `!files <hash>` - Browse files in a commit",
inline=False
)
# Create status view with buttons
view = View(timeout=60)
refresh_btn = Button(label="🔄 Refresh", style=discord.ButtonStyle.secondary, emoji="🔄")
browse_btn = Button(label="📚 Browse Commits", style=discord.ButtonStyle.primary, emoji="📚")
async def refresh_callback(interaction: discord.Interaction):
await ctx.invoke(bot.get_command('status'))
await interaction.response.defer()
async def browse_callback(interaction: discord.Interaction):
await ctx.invoke(bot.get_command('commits'))
refresh_btn.callback = refresh_callback
browse_btn.callback = browse_callback
view.add_item(refresh_btn)
view.add_item(browse_btn)
await ctx.send(embed=embed, view=view)
@bot.command(name="monitor")
@commands.has_permissions(administrator=True)
async def monitor_config(ctx, branch: str = None):
"""Configure monitoring settings (Admin only)"""
global BRANCH
if branch:
BRANCH = branch
await ctx.send(f"✅ Now monitoring branch: **{BRANCH}**")
else:
embed = discord.Embed(
title="⚙️ Monitor Configuration",
description=f"Currently monitoring: **{BRANCH}**\n\n"
f"To change branch:\n`!monitor <branch_name>`\n\n"
f"Example: `!monitor develop`",
color=discord.Color.orange()
)
await ctx.send(embed=embed)
# ---------- BOT EVENTS ----------
@bot.event
async def on_ready():
"""Called when the bot is ready"""
print(f'🤖 Logged in as {bot.user.name} ({bot.user.id})')
print(f'📦 Monitoring: {REPO_OWNER}/{REPO_NAME}@{BRANCH}')
print(f'📺 Channel ID: {CHANNEL_ID}')
print(f'🔗 Gitea URL: {GITEA_BASE_URL}')
# Check channel access
channel = bot.get_channel(CHANNEL_ID)
if channel:
print(f'✅ Found channel: #{channel.name} in {channel.guild.name}')
else:
print(f'❌ Cannot find channel with ID {CHANNEL_ID}')
# Start monitoring task
if not check_commits.is_running():
check_commits.start()
print("✅ Started commit monitoring")
# Set bot presence
await bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.watching,
name=f"{REPO_NAME} commits"
)
)
# ---------- ERROR HANDLING ----------
@bot.event
async def on_command_error(ctx, error):
"""Handle command errors"""
if isinstance(error, commands.CommandNotFound):
embed = discord.Embed(
title="❌ Command Not Found",
description=f"Use `!help` to see available commands.",
color=discord.Color.red()
)
await ctx.send(embed=embed)
elif isinstance(error, commands.MissingPermissions):
embed = discord.Embed(
title="❌ Permission Denied",
description="You don't have permission to use this command.",
color=discord.Color.red()
)
await ctx.send(embed=embed)
elif isinstance(error, commands.MissingRequiredArgument):
embed = discord.Embed(
title="❌ Missing Argument",
description=f"Missing required argument: `{error.param.name}`\n"
f"Use `!help {ctx.command.name}` for usage.",
color=discord.Color.red()
)
await ctx.send(embed=embed)
else:
embed = discord.Embed(
title="❌ Unexpected Error",
description=f"```{str(error)[:500]}```",
color=discord.Color.red()
)
await ctx.send(embed=embed)
print(f"Command error: {error}")
# ---------- RUN BOT ----------
if __name__ == '__main__':
if not DISCORD_TOKEN:
print("❌ Error: DISCORD_BOT_TOKEN not set in .env file")
exit(1)
if CHANNEL_ID == 0:
print("❌ Error: DISCORD_CHANNEL_ID not set in .env file")
exit(1)
try:
bot.run(DISCORD_TOKEN)
except Exception as e:
print(f"❌ Fatal error: {e}")
exit(1)