Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 71 additions & 25 deletions discord_bot/src/bot/commands/analytics_commands.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
"""
Analytics Commands Module

Handles analytics and visualization-related Discord commands.
"""

import discord
from discord import app_commands
from ...utils.analytics import create_top_contributors_chart, create_activity_comparison_chart, create_activity_trend_chart, create_time_series_chart

from src.core.interfaces import IRepoAnalyticsService

from src.utils.analytics.chart_generator import (
create_top_contributors_chart,
create_activity_comparison_chart,
create_activity_trend_chart,
create_time_series_chart,
create_repo_growth_chart
)
from shared.firestore import get_document

class AnalyticsCommands:
"""Handles analytics and visualization Discord commands."""

def __init__(self, bot):
def __init__(self, bot: discord.Client, analytics_service: IRepoAnalyticsService):
self.bot = bot
self.analytics_service = analytics_service

def register_commands(self):
"""Register all analytics commands with the bot."""
self.bot.tree.add_command(self._show_top_contributors_command())
self.bot.tree.add_command(self._show_activity_comparison_command())
self.bot.tree.add_command(self._show_activity_trends_command())
self.bot.tree.add_command(self._show_time_series_command())

self.bot.tree.add_command(self._repo_growth_command())

def _show_top_contributors_command(self):
"""Create the show-top-contributors command."""
@app_commands.command(name="show-top-contributors", description="Show top contributors chart")
async def show_top_contributors(interaction: discord.Interaction):
await interaction.response.defer()
await interaction.response.defer(ephemeral=True)

try:
analytics_data = get_document('repo_stats', 'analytics')
Expand All @@ -42,7 +44,7 @@ async def show_top_contributors(interaction: discord.Interaction):
return

file = discord.File(chart_buffer, filename="top_contributors.png")
await interaction.followup.send("Top contributors by PR count:", file=file)
await interaction.followup.send("Top contributors by PR count:", file=file, ephemeral=True)

except Exception as e:
print(f"Error in show-top-contributors command: {e}")
Expand All @@ -51,10 +53,9 @@ async def show_top_contributors(interaction: discord.Interaction):
return show_top_contributors

def _show_activity_comparison_command(self):
"""Create the show-activity-comparison command."""
@app_commands.command(name="show-activity-comparison", description="Show activity comparison chart")
async def show_activity_comparison(interaction: discord.Interaction):
await interaction.response.defer()
await interaction.response.defer(ephemeral=True)

try:
analytics_data = get_document('repo_stats', 'analytics')
Expand All @@ -70,7 +71,7 @@ async def show_activity_comparison(interaction: discord.Interaction):
return

file = discord.File(chart_buffer, filename="activity_comparison.png")
await interaction.followup.send("Activity comparison chart:", file=file)
await interaction.followup.send("Activity comparison chart:", file=file, ephemeral=True)

except Exception as e:
print(f"Error in show-activity-comparison command: {e}")
Expand All @@ -79,10 +80,9 @@ async def show_activity_comparison(interaction: discord.Interaction):
return show_activity_comparison

def _show_activity_trends_command(self):
"""Create the show-activity-trends command."""
@app_commands.command(name="show-activity-trends", description="Show recent activity trends")
async def show_activity_trends(interaction: discord.Interaction):
await interaction.response.defer()
await interaction.response.defer(ephemeral=True)

try:
analytics_data = get_document('repo_stats', 'analytics')
Expand All @@ -98,7 +98,7 @@ async def show_activity_trends(interaction: discord.Interaction):
return

file = discord.File(chart_buffer, filename="activity_trends.png")
await interaction.followup.send("Recent activity trends:", file=file)
await interaction.followup.send("Recent activity trends:", file=file, ephemeral=True)

except Exception as e:
print(f"Error in show-activity-trends command: {e}")
Expand All @@ -107,17 +107,15 @@ async def show_activity_trends(interaction: discord.Interaction):
return show_activity_trends

def _show_time_series_command(self):
"""Create the show-time-series command."""
@app_commands.command(name="show-time-series", description="Show time series chart with customizable metrics and date range")
@app_commands.describe(
metrics="Comma-separated metrics to display (prs,issues,commits,total)",
days="Number of days to show (7-90, default: 30)"
)
async def show_time_series(interaction: discord.Interaction, metrics: str = "prs,issues,commits", days: int = 30):
await interaction.response.defer()
await interaction.response.defer(ephemeral=True)

try:
# Validate inputs
if days < 7 or days > 90:
await interaction.followup.send("Days must be between 7 and 90.", ephemeral=True)
return
Expand Down Expand Up @@ -148,10 +146,58 @@ async def show_time_series(interaction: discord.Interaction, metrics: str = "prs
return

file = discord.File(chart_buffer, filename="time_series.png")
await interaction.followup.send(f"Time series chart for last {days} days:", file=file)
await interaction.followup.send(f"Time series chart for last {days} days:", file=file, ephemeral=True)

except Exception as e:
print(f"Error in show-time-series command: {e}")
await interaction.followup.send("Error generating time series chart.", ephemeral=True)

return show_time_series
return show_time_series

def _repo_growth_command(self):

@app_commands.command(
name="repo_growth",
description="Shows a chart of the repository's cumulative growth."
)
@app_commands.checks.has_permissions(administrator=True)
async def cmd(interaction: discord.Interaction):
try:
await interaction.response.defer(thinking=True, ephemeral=True)

stats = await self.analytics_service.get_code_frequency_stats()

if not stats:
await interaction.followup.send(
"Sorry, I couldn't fetch the repository stats. This can happen if GitHub "
"is caching the data. Please try again in a few minutes.",
ephemeral=True
)
return

chart_buffer = create_repo_growth_chart(stats)

if chart_buffer is None:
await interaction.followup.send("An error occurred while generating the chart.", ephemeral=True)
return

file = discord.File(fp=chart_buffer, filename="repo_growth.png")

embed = discord.Embed(
title="Repository Growth",
description="Here is the cumulative net line-of-code growth over time.",
color=discord.Color.blue()
)
embed.set_image(url="attachment://repo_growth.png")
embed.set_footer(text="Data sourced from GitHub API")

await interaction.followup.send(embed=embed, file=file, ephemeral=True)

except Exception as e:
print(f"Error in /repo_growth command: {e}")
if not interaction.response.is_done():
await interaction.response.send_message("An unexpected error occurred.", ephemeral=True)
else:
await interaction.followup.send("An unexpected error occurred.", ephemeral=True)

return cmd
46 changes: 46 additions & 0 deletions discord_bot/src/core/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from .singleton import Singleton
from .interfaces import (
IRepoAnalyticsService,
IGuildService,
IGitHubService,
IRoleService,
INotificationService
)
from .services import (
GitHubAnalyticsService,
GuildService,
GitHubService,
RoleService,
NotificationService
)

class ServiceContainer:
def __init__(self):
self._services = {}

def register_singleton(self, interface, implementation):
self._services[interface] = Singleton(implementation)
print(f"Registered {interface.__name__} -> {implementation.__name__}")

def resolve(self, interface):
try:
implementation_factory = self._services[interface]
return implementation_factory.get_instance()
except KeyError:
raise Exception(f"No service registered for interface: {interface.__name__}")

def setup_dependencies() -> ServiceContainer:
container = ServiceContainer()

# container.register_singleton(IGuildService, GuildService)
# container.register_singleton(IGitHubService, GitHubService)
# container.register_singleton(IRoleService, RoleService)
# container.register_singleton(INotificationService, NotificationService)

container.register_singleton(IRepoAnalyticsService, GitHubAnalyticsService)

print("-" * 30)
print("Service container setup complete.")
print("-" * 30)

return container
21 changes: 21 additions & 0 deletions discord_bot/src/core/singleton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Singleton wrapper for the service container.
"""

class Singleton:
"""
A wrapper to ensure a class is only instantiated once
by the ServiceContainer.
"""
def __init__(self, implementation_class):
self._implementation_class = implementation_class
self._instance = None

def get_instance(self):
"""
Get the singleton instance.
Creates it if it doesn't exist yet.
"""
if self._instance is None:
self._instance = self._implementation_class()
return self._instance
17 changes: 17 additions & 0 deletions discord_bot/src/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Service implementations.
"""

from .guild_service import GuildService
from .github_service import GitHubService
from .role_service import RoleService
from .notification_service import NotificationService
from .analytics_service import GitHubAnalyticsService # ✨ ADD THIS LINE

__all__ = [
'GuildService',
'GitHubService',
'RoleService',
'NotificationService',
'GitHubAnalyticsService', # ✨ AND ADD THIS LINE
]
Empty file.