massive fixes
This commit is contained in:
46
src/ComitsPaginatedView.py
Normal file
46
src/ComitsPaginatedView.py
Normal 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
57
src/CommitsActionView.py
Normal 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
36
src/CommitsSelectMenu.py
Normal 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
32
src/FileBrowserSelect.py
Normal 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
55
src/PaginatedView.py
Normal 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
22
src/SearchModal.py
Normal 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
853
src/main.py
Normal 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)
|
||||
Reference in New Issue
Block a user