Discord bot for GitHub repo and issue info retrieval.
#!/usr/bin/env python3
import re
import discord
from discord.ext import commands
from discord import app_commands
from gitscribe.apis.github import get_repository_info, get_issue_info, get_repo_issues
from gitscribe.apis.github import get_repository_info, get_issue_info, get_repo_issues, issues_label_stats
from gitscribe.utils import format_github_info


__all__ = ['bot']


# Bot's intents
intents = discord.Intents.default()
intents.typing = False
intents.presences = False
intents.message_content = True


# Bot instance
bot = commands.Bot(
    command_prefix='!',
    intents=intents,
    disable_auto_responses=True
)


@bot.event
async def on_ready():
    print(f'Logged in as {bot.user.name}')

    for guild in bot.guilds:
        try:
            synced = await bot.tree.sync()
            
        except Exception as e:
            print(str(e))
        

@bot.event
async def on_message(message):  
    if message.author.id == bot.user.id:
        return           
    await bot.process_commands(message)


# Hybrid commands
@bot.hybrid_command(name="repo", with_app_command=True, description="Create wallets for all members (initialize)")
async def repo(ctx: commands.Context, repo_link):
    # In case of prefixed command repo_link can be null
    if not repo_link:
        await ctx.send('Please provide a valid repo link')
        return 
    # Extract the GitHub username and repository name from the link
    match = re.match(r'https://github.com/([^/]+)/([^/]+)', repo_link)
    if not match:
        await ctx.send('Invalid GitHub repo link format. Please provide a valid link.')
        return

    username, repo_name = match.group(1), match.group(2)

    repo_data = get_repository_info(username, repo_name)

    if repo_data:
        all_issues = get_repo_issues(username, repo_name)

        good_first_issue_count = issues_label_stats(all_issues).get("good first issue", 0)
        await ctx.send(format_github_info(repo_data, len(all_issues), good_first_issue_count))
    else:
        await ctx.send('Repository not found or an error occurred.')


@bot.hybrid_command(name="issue", with_app_command=True, description="Get info about an issue")
async def issue(ctx: commands.Context, issue_link):

    # In case of prefixed command issue_link can be null
    if not issue_link:
        await ctx.send('Please provide a valid repo link')
        return
    # Use Regex to extract the GitHub username, repository name, and issue number from the link
    match = re.match(
        r'https://github.com/([^/]+)/([^/]+)/issues/(\d+)', issue_link)
    if not match:
        await ctx.send('Invalid GitHub issue link format. Please provide a valid link.')
        return

    username, repo_name, issue_number = match.group(
        1), match.group(2), match.group(3)

    repo_data = get_repository_info(username, repo_name)
    issue_data = get_issue_info(username, repo_name, issue_number)

    if repo_data and issue_data:
        await ctx.send(format_github_info(repo_data, None, None, issue_data))
    else:
        await ctx.send('Issue or repository not found or an error occurred.')


# Error handling
@bot.event   
async def on_command_error(ctx, error):
    print(error)
    embed = discord.Embed(title=error, color=0x00ff00)
    await ctx.send(embed=embed)
gitscribe/_discord.py
oss-reva/gitscribe
import requests


def get_repository_info(username, repo_name) -> dict:
    url = f'https://api.github.com/repos/{username}/{repo_name}'
    response = requests.get(url)

    return response.json() if response.status_code == 200 else None


def get_issue_info(username, repo_name, issue_number) -> dict:
    url = f'https://api.github.com/repos/{username}/{repo_name}/issues/{issue_number}'
    response = requests.get(url)

    return response.json() if response.status_code == 200 else None


def get_repo_issues(username, reponame, auth_token=None) -> list[dict]:
    issues = []

    page_n = 1
    while 1:
        rs = requests.get(
            url=f"https://api.github.com/repos/{username}/{reponame}/issues",
            params={
                "filter": "all",
                "per_page": 100,
                "page": page_n
            },
            headers={
                "Authorization": f"Bearer {auth_token}" if auth_token else None
            }
        )

        if rs.status_code == 200:
            sue = rs.json()

            issues.extend(sue)
            if len(sue) < 100:
                break
        else:
            break

        page_n += 1
    return issues


def issue_get_labels(issue: dict) -> dict:
    rs = {
        "labels": []
    }

    if issue.get("labels"):
        rs["labels"] = [l["name"] for l in issue["labels"]]

    return rs


def issues_label_stats(issues: list[dict]) -> dict:
    rs = {}

    for i in issues:
        for l in issue_get_labels(i):
            if l not in rs:
                rs[l] = 0
            rs[l] += 1
    return rs

gitscribe/apis/github.py
oss-reva/gitscribe


NAME = "gitscribe"
VERSION = "0.1.0"

if __name__ == "__main__":
    print(NAME)
    print(VERSION)

    quit()

gitscribe/consts.py
oss-reva/gitscribe
import re


def format_github_info(repo_data, total_issues=None, good_first_issue_count=None, issue_data=None):
    description = repo_data['description'] or '*No description*'
    stars = repo_data['stargazers_count']
    forks = repo_data['forks_count']

    formatted_info = (
        f"__**Repository Info**__\n"
        f"**Repository Name:** {repo_data['name']}\n"
        f"**Description:** {description}\n"
        f"**Stars:** {stars}\n"
        f"**Forks:** {forks}\n"
    )

    if total_issues is not None:
        formatted_info += (
            f"**Total Issues:** {total_issues}\n"
            f"**Good First Issues:** {good_first_issue_count}\n"
        )

    if issue_data:
        title = issue_data['title']
        number = issue_data['number']
        created_by = issue_data['user']['login']
        state = 'Open' if issue_data['state'] == 'open' else 'Closed'
        assigned = issue_data['assignee']['login'] if issue_data['assignee'] else '*Not assigned*'
        description = re.sub(
            r'(https?://[^\s]+)', r'`\1`', issue_data['body'][:1000])

        formatted_info += (
            f"\n__**Issue Info**__\n"
            f"**Issue Title:** {title}\n"
            f"**Issue Number:** {number}\n"
            f"**Created By:** {created_by}\n"
            f"**Status:** {state}\n"
            f"**Assigned:** {assigned}\n"
            f"\n__**Description (Partial)**__\n{description}\n"
            f"\nFor the full description, please view the issue on GitHub."
        )

    return formatted_info

gitscribe/utils.py
oss-reva/gitscribe
from setuptools import setup, find_packages
from gitscribe.consts import NAME, VERSION

with open("requirements.txt") as f:
    requirements = f.read().splitlines()

with open("README.rst", "r", encoding="utf-8") as readme_file:
    long_description = readme_file.read()

setup(
    name=NAME,
    version=VERSION,
    description="A Discord bot that provides information about GitHub repositories and issues.",
    long_description=long_description,
    author="SNG Viraj Reddy",
    author_email="vir200319@gmail.com",
    url="https://github.com/oss-reva/gitscribe",
    license="MIT",
    classifiers=[
        "Development Status :: 5 - Production/Stable",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.6",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Topic :: Software Development :: Libraries :: Python Modules",
        "Topic :: Utilities",
    ],
    packages=find_packages(),
    install_requires=requirements,
    scripts=[
        "scripts/gitscribe"
    ]
)

setup.py
oss-reva/gitscribe