Skip to content
Merged

Dev #55

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ src/pip-delete-this-directory.txt
**/.vs
**/.vscode
**/.DS_Store

pbrVenv
src/lib
.env
2 changes: 1 addition & 1 deletion PyBugReporter/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.9b0'
__version__ = '1.0.9b0.dev3'
3 changes: 2 additions & 1 deletion PyBugReporter/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
python-graphql-client~=0.4.3
python-graphql-client~=0.4.3
discord.py~=2.0.1
79 changes: 60 additions & 19 deletions PyBugReporter/src/BugReporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import traceback
from functools import wraps
from PyBugReporter.src.DiscordBot import DiscordBot

from python_graphql_client import GraphqlClient

Expand All @@ -25,20 +26,31 @@ class BugHandler:
repoName: str = ''
orgName: str = ''
test: bool = False
useDiscord: bool = False
botToken: str = ''
channelId: str | int = ''

def __init__(self, githubKey: str, repoName: str, orgName: str, test: bool) -> None:
def __init__(self, githubKey: str, repoName: str, orgName: str, test: bool, useDiscord: bool = False, botToken: str = "", channelId: str | int = "") -> None:
"""Saves the given information in the BugHandler object.

Args:
githubKey (str): the key to use to make the issue
repoName (str): the name of the repo to report to
orgName (str): the organization of the repo
test (bool): whether or not bugs in this code should actually be reported
useDiscord (bool): whether to send the bug report to Discord
botToken (str): the token for the Discord bot
channelId (str | int): the ID of the Discord channel to send messages to
"""
self.githubKey = githubKey
self.repoName = repoName
self.orgName = orgName
self.test = test
self.useDiscord = useDiscord

if useDiscord:
self.botToken = botToken
self.channelId = channelId

class BugReporter:
"""Sends errors to their corresponding repos.
Expand All @@ -64,7 +76,7 @@ def __init__(self, repoName: str, extraInfo: bool, **kwargs) -> None:
self.kwargs = kwargs

@classmethod
def setVars(cls, githubKey: str, repoName: str, orgName: str, test: bool) -> None:
def setVars(cls, githubKey: str, repoName: str, orgName: str, test: bool, useDiscord: bool = False, botToken: str = "", channelId: str = "") -> None:
"""Sets the necessary variables to make bug reports.

Args:
Expand All @@ -73,7 +85,7 @@ def setVars(cls, githubKey: str, repoName: str, orgName: str, test: bool) -> Non
orgName (str): the name of the organization
test (bool): whether to run in testing mode
"""
cls.handlers[repoName] = BugHandler(githubKey, repoName, orgName, test)
cls.handlers[repoName] = BugHandler(githubKey, repoName, orgName, test, useDiscord, botToken, channelId)

def __call__(self, func: callable) -> None:
"""Decorator that catches exceptions and sends a bug report to the github repository.
Expand Down Expand Up @@ -118,26 +130,51 @@ def _handleError(self, e: Exception, repoName: str, *args, **kwargs) -> None:
if self.extraInfo:
description += f"\nExtra Info: {self.kwargs}"

# shortened description for discord if too long (shortens the error text)
start = f"# {title}\n\nType: {excType}\nError text: "
compress = f"{e}\nTraceback: {traceback.format_exc()}"
end = f"\n\nFunction Name: {functionName}\nArguments: {args}\nKeyword Arguments: {kwargs}"
if self.extraInfo:
end += f"\nExtra Info: {self.kwargs}"

staticLength = len(start) + len(end)
if staticLength > 2000:
shortDescription = f"# {title}\n\n" + description[:2000 - len(f"# {title}\n\n") - 3] + "..."
else:
shortDescription = f"{start}{compress[:2000 - staticLength]}{end}"

print(f"SHORT DESCRIPTION with length {len(shortDescription)}:\n{shortDescription}")


# Check if we need to send a bug report
if not self.handlers[repoName].test:
self._sendBugReport(repoName, title, description)
self._sendBugReport(repoName, title, description, shortDescription)

print(title)
print(description)
raise e

def _sendBugReport(self, repoName: str, errorTitle: str, errorMessage: str) -> None:
def _sendBugReport(self, repoName: str, errorTitle: str, errorMessage: str, shortErrorMessage: str) -> None:
"""Sends a bug report to the Github repository.

Args:
errorTitle (str): the title of the error
errorMessage (str): the error message
"""
"""
asyncio.run(self._sendBugReport_async(repoName, errorTitle, errorMessage, shortErrorMessage))

async def _sendBugReport_async(self, repoName: str, errorTitle: str, errorMessage: str, shortErrorMessage: str) -> None:
"""Sends a bug report to the Github repository asynchronously.

Args:
errorTitle (str): the title of the error
errorMessage (str): the error message
"""
client = GraphqlClient(endpoint="https://api.github.com/graphql")
headers = {"Authorization": f"Bearer {self.handlers[repoName].githubKey}"}

# query variables
repoId = self._getRepoId(self.handlers[repoName])
repoId = await self._getRepoId_async(self.handlers[repoName])
bugLabel = "LA_kwDOJ3JPj88AAAABU1q15w"
autoLabel = "LA_kwDOJ3JPj88AAAABU1q2DA"

Expand Down Expand Up @@ -171,10 +208,15 @@ def _sendBugReport(self, repoName: str, errorTitle: str, errorMessage: str) -> N
}
}

issueExists = self._checkIfIssueExists(self.handlers[repoName], errorTitle)
issueExists = await self._checkIfIssueExists_async(self.handlers[repoName], errorTitle)

if (issueExists == False):
result = asyncio.run(client.execute_async(query=createIssue, variables=variables, headers=headers))
# Send to Discord if applicable
if self.handlers[repoName].useDiscord:
discordBot = DiscordBot(self.handlers[repoName].botToken, self.handlers[repoName].channelId)
await discordBot.send_message(shortErrorMessage, issueExists)

if (not issueExists):
result = await client.execute_async(query=createIssue, variables=variables, headers=headers)
print('\nThis error has been reported to the Tree Growth team.\n')

issueId = result['data']['createIssue']['issue']['id'] # Extract the issue ID
Expand All @@ -191,20 +233,19 @@ def _sendBugReport(self, repoName: str, errorTitle: str, errorMessage: str) -> N
"""

# Replace with your actual project ID
projectId = self.getProjectId(repoName, "Tree Growth Projects")
projectId = await self.getProjectId_async(repoName, "Tree Growth Projects")

variables = {
"projectId": projectId,
"contentId": issueId
}

# Execute the mutation to add the issue to the project
asyncio.run(client.execute_async(query=addToProject, variables=variables, headers=headers))
await client.execute_async(query=addToProject, variables=variables, headers=headers)
else:
print('\nOur team is already aware of this issue.\n')

def getProjectId(self, repoName: str, projectName: str) -> str:
"""Retrieves the GitHub project ID for a specified repository and project name."""
async def getProjectId_async(self, repoName: str, projectName: str) -> str:
client = GraphqlClient(endpoint="https://api.github.com/graphql")
headers = {"Authorization": f"Bearer {self.handlers[repoName].githubKey}"}

Expand All @@ -228,7 +269,7 @@ def getProjectId(self, repoName: str, projectName: str) -> str:
}

# Execute the query
response = asyncio.run(client.execute_async(query=query, variables=variables, headers=headers))
response = await client.execute_async(query=query, variables=variables, headers=headers)
projects = response["data"]["repository"]["projectsV2"]["nodes"]

# Find the project with the matching name and return its ID
Expand All @@ -238,7 +279,7 @@ def getProjectId(self, repoName: str, projectName: str) -> str:

raise ValueError(f"Project '{projectName}' not found in repository '{repoName}'.")

def _checkIfIssueExists(self, handler: BugHandler, errorTitle: str) -> bool:
async def _checkIfIssueExists_async(self, handler: BugHandler, errorTitle: str) -> bool:
"""Checks if an issue already exists in the repository.

Args:
Expand Down Expand Up @@ -276,7 +317,7 @@ def _checkIfIssueExists(self, handler: BugHandler, errorTitle: str) -> bool:
"labels": autoLabel,
}

result = asyncio.run(client.execute_async(query=findIssue, variables=variables, headers=headers))
result = await client.execute_async(query=findIssue, variables=variables, headers=headers)
nodes = result['data']['organization']['repository']['issues']['nodes']

index = 0
Expand All @@ -292,7 +333,7 @@ def _checkIfIssueExists(self, handler: BugHandler, errorTitle: str) -> bool:

return issueExists

def _getRepoId(self, handler: BugHandler) -> str:
async def _getRepoId_async(self, handler: BugHandler) -> str:
"""Gets the repository ID.

Args:
Expand All @@ -318,7 +359,7 @@ def _getRepoId(self, handler: BugHandler) -> str:
"name": handler.repoName
}

repoID = asyncio.run(client.execute_async(query=getID, variables=variables, headers=headers))
repoID = await client.execute_async(query=getID, variables=variables, headers=headers)
return repoID['data']['repository']['id']

@classmethod
Expand Down
79 changes: 79 additions & 0 deletions PyBugReporter/src/DiscordBot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import asyncio
import discord

HISTORY_LIMIT = 20
EMOJI = "‼"

class DiscordBot(discord.Client):
"""
A simple Discord bot that forwards the bug reports to a given Discord channel.

Attributes:
token (str): bot token
channel_id (int): the ID of the channel to send messages to
_message (str): message to send
_alreadySent (bool): whether the message has already been sent
_done_future (asyncio.Future): a future that is set when the bot is done
"""
def __init__(self, token: str, channelId: str | int) -> None:
"""
Initializes the Discord bot with the given token and channel ID.

Args:
token (str): bot token
channel_id (int): the ID of the channel to send messages to
"""
self.token = token
self.channelId = int(channelId)
self._message = None
self._alreadySent = False
self._doneFuture = None

intents = discord.Intents(emojis = True,
guild_reactions = True,
message_content = True,
guild_messages = True,
guilds = True)
super().__init__(intents=intents)

async def send_message(self, message, alreadySent = False):
"""
Sends a message to the specified channel by setting the variables and starting the bot, then turning it off when finished.

Args:
message (str): The message to send.
alreadySent (bool): Whether the message has already been sent.
"""
self._message = message
self._alreadySent = alreadySent
self._doneFuture = asyncio.get_running_loop().create_future()
print("Starting bot...")
# Start the bot as a background task
asyncio.create_task(self.start(self.token))
# Wait until the message is sent and the bot is closed
await self._doneFuture

async def on_ready(self):
"""
Called when the bot is ready. Also sends the message to the specified channel, or reacts if it's been sent.
"""
try:
channel = await self.fetch_channel(self.channelId)
if channel and not self._alreadySent:
await channel.send(self._message)
print(f"Sent message to channel {self.channelId}")
elif channel and self._alreadySent:
async for message in channel.history(limit=HISTORY_LIMIT):
if message.content == self._message:
await message.add_reaction(EMOJI)
break
else:
print(f"Channel with ID {self.channelId} not found.")
except Exception as e:
print(f"Error sending message: {e}")
finally:
print("Shutting down bot...")
await self.close()
# Mark the future as done so send_message can return
if self._doneFuture and not self._doneFuture.done():
self._doneFuture.set_result(True)
Loading