Skip to content

Commit be38ada

Browse files
authored
Merge pull request #37 from PrithvijitBose/feature/profile-card
feat: enhance profile command with rich embeds, 30-day contribution m…
2 parents e79175a + 75bf81a commit be38ada

1 file changed

Lines changed: 159 additions & 11 deletions

File tree

src/ghdcbot/bot.py

Lines changed: 159 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
import asyncio
66
import logging
7+
8+
from datetime import datetime, timedelta, timezone
9+
from typing import Any
10+
711
from datetime import datetime
812
from typing import Any, Optional
913

14+
1015
import discord
1116
from discord import app_commands
1217

@@ -15,7 +20,12 @@
1520
from ghdcbot.config.loader import load_config
1621
from ghdcbot.core.errors import ConfigError
1722
from ghdcbot.engine.identity_linking import IdentityLinkService, LinkClaim
18-
from ghdcbot.engine.metrics import build_contribution_summary_message
23+
from ghdcbot.engine.metrics import (
24+
build_contribution_summary_message,
25+
get_contribution_metrics,
26+
rank_by_activity,
27+
get_rank_for_user,
28+
)
1929
from ghdcbot.engine.issue_assignment import (
2030
resolve_discord_to_github,
2131
)
@@ -435,21 +445,159 @@ async def profile_cmd(
435445
target = interaction.user if viewing_self else contributor
436446
assert target is not None
437447
discord_user_id = str(target.id)
438-
lines: list[str] = []
439-
if not viewing_self:
440-
lines.append(f"**Profile for {target.mention}**")
441448

442-
lines.extend(
443-
_identity_status_lines(storage, config, discord_user_id, viewing_self)
449+
# 1. Fetch identity status from storage
450+
get_status = getattr(storage, "get_identity_status", None)
451+
max_age_days = None
452+
if getattr(config, "identity", None) is not None:
453+
max_age_days = getattr(config.identity, "verified_max_age_days", None)
454+
status = get_status(discord_user_id, max_age_days=max_age_days) if callable(get_status) else {}
455+
456+
github_user = status.get("github_user")
457+
st = status.get("status") or "not_linked"
458+
459+
# 2. Determine verification badge & color
460+
if st == "verified":
461+
status_label = "Verified ✅"
462+
color = discord.Color.green()
463+
elif st == "verified_stale":
464+
status_label = "Verified ⚠️ (Stale)"
465+
color = discord.Color.gold()
466+
elif st == "pending":
467+
status_label = "Pending ⏳"
468+
color = discord.Color.orange()
469+
else:
470+
status_label = "Not linked ❌"
471+
color = discord.Color.red()
472+
473+
# 3. Create the Embed
474+
embed = discord.Embed(
475+
color=color,
476+
timestamp=datetime.now(timezone.utc)
444477
)
445-
lines.append(
446-
await _format_social_profiles_line(social_service, discord_user_id, viewing_self)
478+
479+
# Show github user link and avatar if linked, and set author details
480+
if github_user:
481+
github_profile_url = f"https://github.com/{github_user}"
482+
embed.set_thumbnail(url=f"https://github.com/{github_user}.png")
483+
embed.set_author(
484+
name=f"{target.display_name} (@{github_user})",
485+
icon_url=target.display_avatar.url if target.display_avatar else None,
486+
url=github_profile_url,
487+
)
488+
embed.add_field(
489+
name="GitHub Account",
490+
value=f"[{github_user}]({github_profile_url})",
491+
inline=True,
492+
)
493+
else:
494+
embed.set_author(
495+
name=f"{target.display_name}",
496+
icon_url=target.display_avatar.url if target.display_avatar else None,
497+
)
498+
embed.add_field(name="GitHub Account", value="Not Linked ❌", inline=True)
499+
500+
embed.add_field(name="Verification Status", value=status_label, inline=True)
501+
502+
# Add verification date if verified
503+
verified_at = status.get("verified_at")
504+
if verified_at:
505+
try:
506+
dt = datetime.fromisoformat(verified_at.replace("Z", "+00:00"))
507+
verified_at_str = dt.strftime("%Y-%m-%d %H:%M UTC")
508+
except (ValueError, TypeError):
509+
verified_at_str = verified_at
510+
embed.add_field(name="Verified At", value=verified_at_str, inline=True)
511+
elif github_user:
512+
embed.add_field(name="Verified At", value="—", inline=True)
513+
514+
# Add warnings in the description if verification is stale
515+
if status.get("is_stale"):
516+
if viewing_self:
517+
embed.description = "⚠️ **Warning:** Your identity verification is stale. Use `/verify-link` to refresh it."
518+
else:
519+
embed.description = "⚠️ **Warning:** Their identity verification is stale."
520+
521+
# 4. Fetch contribution metrics if GitHub user exists
522+
if github_user:
523+
metrics_available = True
524+
try:
525+
now_utc = datetime.now(timezone.utc)
526+
start_30 = now_utc - timedelta(days=30)
527+
528+
def _fetch_metrics():
529+
metrics_list = get_contribution_metrics(storage, start_30, now_utc)
530+
user_metrics = next((m for m in metrics_list if m.github_user == github_user), None)
531+
ranked_30 = rank_by_activity(metrics_list)
532+
rank = get_rank_for_user(ranked_30, github_user)
533+
return user_metrics, rank
534+
535+
user_metrics, rank = await asyncio.to_thread(_fetch_metrics)
536+
except Exception as e:
537+
logging.getLogger("ghdcbot.bot").debug(
538+
"Error fetching contribution metrics for /profile: %s", e
539+
)
540+
metrics_available = False
541+
user_metrics, rank = None, None
542+
543+
if user_metrics:
544+
metrics_lines = [
545+
f"📝 **PRs:** {user_metrics.prs_opened} Opened / {user_metrics.prs_merged} Merged",
546+
f"👀 **Reviews:** {user_metrics.reviews_submitted} Submitted",
547+
f"💬 **Issues & Comments:** {user_metrics.issues_opened} Issues / {user_metrics.comments} Comments",
548+
]
549+
if rank is not None:
550+
metrics_lines.append(f"🏆 **Rank:** #{rank} in server activity")
551+
552+
embed.add_field(
553+
name="📊 Contributions (Last 30 Days)",
554+
value="\n".join(metrics_lines),
555+
inline=False,
556+
)
557+
elif metrics_available:
558+
embed.add_field(
559+
name="📊 Contributions (Last 30 Days)",
560+
value="No activity recorded in the last 30 days.",
561+
inline=False,
562+
)
563+
else:
564+
embed.add_field(
565+
name="📊 Contributions (Last 30 Days)",
566+
value="Contribution data is temporarily unavailable.",
567+
inline=False,
568+
)
569+
def _truncate_embed_field_value(value: str, limit: int = 1024) -> str:
570+
if len(value) <= limit:
571+
return value
572+
return f"{value[:limit - 1]}…"
573+
# 5. Fetch and add Social Profiles
574+
socials_line = await _format_social_profiles_line(social_service, discord_user_id, viewing_self)
575+
if socials_line.startswith("**Social Profiles:**"):
576+
socials_value = socials_line.replace("**Social Profiles:**", "").strip()
577+
else:
578+
socials_value = socials_line.strip()
579+
embed.add_field(
580+
name="Connected Socials",
581+
value=_truncate_embed_field_value(socials_value),
582+
inline=False,
447583
)
448-
lines.append(await _format_roles_line(discord_reader, discord_user_id))
449-
await interaction.followup.send(
450-
"\n".join(lines), ephemeral=True, suppress_embeds=True
584+
# 6. Fetch and add Discord Roles
585+
roles_line = await _format_roles_line(discord_reader, discord_user_id)
586+
if roles_line.startswith("**Roles:**"):
587+
roles_value = roles_line.replace("**Roles:**", "").strip()
588+
else:
589+
roles_value = roles_line.strip()
590+
591+
embed.add_field(
592+
name="Server Roles",
593+
value=_truncate_embed_field_value(roles_value),
594+
inline=False,
451595
)
452596

597+
embed.set_footer(text="Gitcord Automation Engine")
598+
599+
await interaction.followup.send(embed=embed, ephemeral=True)
600+
453601
@tree.command(
454602
name="summary",
455603
description="Show contribution metrics (last 7 and 30 days; optional Discord member)",

0 commit comments

Comments
 (0)