Skip to content
Merged

Stg #65

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2783c0e
Auto-updated version from 1.0.8b2 to 1.0.8b2.dev0
actions-user Mar 12, 2025
b859e8f
Auto-updated version from 1.0.9 to 1.0.9b0
actions-user Mar 12, 2025
5cb2e06
Merge remote-tracking branch 'origin/stg' into dev
actions-user Mar 12, 2025
e953186
Auto-updated version from 1.0.9b0 to 1.0.9b0.dev0
actions-user Mar 12, 2025
450660b
made bot
epbay01 May 15, 2025
e46d47a
a bunch of fixes, it still doesnt work
epbay01 May 15, 2025
fa408ce
final fixes!
epbay01 May 16, 2025
271895c
adding reactions
epbay01 May 16, 2025
fa29c64
bot makes a future (promise) and adds it to the loop, then sets it to…
epbay01 May 16, 2025
ecab60c
docs
epbay01 May 16, 2025
27c9a3e
std check stuff (docs)
epbay01 May 16, 2025
e5eb303
more std check
epbay01 May 16, 2025
c689622
Discord bot (#54)
epbay01 May 16, 2025
2931b6a
Auto-updated version from 1.0.9b0.dev0 to 1.0.9b0.dev1
actions-user May 16, 2025
5f6cb63
added to the requirements.txt file
epbay01 May 21, 2025
d500daa
added to the requirements.txt file (#56)
koubarlow May 21, 2025
fe3f889
Auto-updated version from 1.0.9b0.dev1 to 1.0.9b0.dev2
actions-user May 21, 2025
72310ed
added compressable middle part of the error message so that we can ha…
epbay01 May 21, 2025
88c46bb
Merge branch 'dev' into 2000-char-limit
epbay01 May 21, 2025
4c17110
traceback test
epbay01 May 21, 2025
4273871
Merge branch '2000-char-limit' of https://github.com/byuawsfhtl/PyBug…
epbay01 May 21, 2025
011ed40
2000 char limit (#60)
epbay01 May 22, 2025
eafd91f
Auto-updated version from 1.0.9b0.dev2 to 1.0.9b0.dev3
actions-user May 22, 2025
838627c
Dev (#55)
koubarlow May 22, 2025
d2eea51
Auto-updated version from 1.0.9b0.dev3 to 1.0.9b1
actions-user May 22, 2025
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.9'
__version__ = '1.0.9b1'
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