From c54e1073f74f240be0f9b957d95f4a2a798d503b Mon Sep 17 00:00:00 2001 From: travis weir Date: Sun, 16 Apr 2023 20:50:03 -0500 Subject: [PATCH 01/66] CI spring cleaning --- .github/workflows/main.yml | 4 ++-- pylintrc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eb05d9c3..71beb8ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,9 +11,9 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 with: python-version: '3.10.6' diff --git a/pylintrc b/pylintrc index cedd549e..9d342e7e 100755 --- a/pylintrc +++ b/pylintrc @@ -505,4 +505,4 @@ known-third-party=enchant # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception \ No newline at end of file +overgeneral-exceptions=builtins.Exception \ No newline at end of file From 43d44a5fe934741e496fbae6a29bc898b2dd0ecf Mon Sep 17 00:00:00 2001 From: Guinea Wheek Date: Tue, 18 Apr 2023 17:46:28 -0700 Subject: [PATCH 02/66] Initial version of %ftc matches Allows displaying upcoming/past matches for a team. TODO: - buttons to select other events - handle division finals better - awards? --- dozer/cogs/ftc.py | 239 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 222 insertions(+), 17 deletions(-) diff --git a/dozer/cogs/ftc.py b/dozer/cogs/ftc.py index 451ec601..41f47480 100755 --- a/dozer/cogs/ftc.py +++ b/dozer/cogs/ftc.py @@ -2,13 +2,14 @@ import json from asyncio import sleep -from datetime import datetime +import datetime from urllib.parse import urljoin, urlencode import base64 import aiohttp import async_timeout import discord +import pprint from discord.ext import commands from discord.utils import escape_markdown @@ -17,6 +18,11 @@ embed_color = discord.Color(0xed791e) +__all__ = ['FTCEventsClient', 'FTCInfo', 'setup'] + +def get_none_strip(s, key): + """Ensures that a get always returns a stripped string.""" + return (str(s.get(key, "")) or "").strip() class FTCEventsClient: """ @@ -25,7 +31,7 @@ class FTCEventsClient: def __init__(self, username: str, token: str, aiohttp_session: aiohttp.ClientSession, base_url: str = "https://ftc-api.firstinspires.org/v2.0", ratelimit: bool = True): - self.last_req: datetime = datetime.now() + self.last_req: datetime.datetime = datetime.datetime.now() self.ratelimit: bool = ratelimit self.base: str = base_url self.http: aiohttp.ClientSession = aiohttp_session @@ -35,15 +41,15 @@ async def req(self, endpoint, season=None): """Make an async request at the specified endpoint, waiting to let the ratelimit cool off.""" if season is None: - season = self.get_season() + season = FTCEventsClient.get_season() if self.ratelimit: # this will delay a request to avoid the ratelimit - now = datetime.now() + now = datetime.datetime.now() diff = (now - self.last_req).total_seconds() self.last_req = now - if diff < 2.2: # have a 200 ms fudge factor - await sleep(2.2 - diff) + if diff < 0.5: # have a 200 ms fudge factor + await sleep(0.5 - diff) tries = 0 while True: try: @@ -53,16 +59,111 @@ async def req(self, endpoint, season=None): if tries > 3: raise - def get_season(self): + @staticmethod + def get_season(): """Fetches the current season, based on typical kickoff date.""" - today = datetime.today() + today = datetime.datetime.today() year = today.year # ftc kickoff is always the 2nd saturday of september - kickoff = [d for d in [datetime(year=year, month=9, day=i) for i in range(8, 15)] if d.weekday() == 5][0] + kickoff = [d for d in [datetime.datetime(year=year, month=9, day=i) for i in range(8, 15)] if d.weekday() == 5][0] if kickoff > today: return today.year - 1 return today.year + + @staticmethod + def date_parse(date_str): + """Takes in date strings from FTC-Events and parses them into a datetime.datetime""" + return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S") + + @staticmethod + def team_fmt(team, team_num=None): + """TBA-formats a team.""" + t = str(team['teamNumber']) + if team['surrogate']: + # indicate surrogate status by italicizing them + t = f"*{t}*" + if team['teamNumber'] == team_num: + # underline the team + t = f"__{t}__" + if team['noShow'] or team['dq']: + # cross out the team + t = f"~~{t}~~" + return t + + @staticmethod + def get_url_for_match(szn: int, ecode: str, match: dict): + """Produces a URL for a match dict + some other info.""" + base = f"https://ftc-events.firstinspires.org/{szn}/{ecode}/" + if match['tournamentLevel'] == "SEMIFINAL": + return base + f"semifinal/{match['series']}/{match['matchNumber']}" + if match['tournamentLevel'] == "FINAL": + return base + f"final/{match['series']}/{match['matchNumber']}" + else: + return base + f"qualifications/{match['matchNumber']}" + + @staticmethod + def add_schedule_to_embed(embed: discord.Embed, schedule: list, team_num: int, szn: int, ecode: str): + """Adds a schedule to an embed conditioned on a team number.""" + for m in schedule: + played = m['scoreRedFinal'] is not None + red_alliance = [] + blu_alliance = [] + team_alliance = None + for team in m['teams']: + alliance = "blue" + if (team['station'] or "").startswith("Red"): + alliance = "red" + red_alliance.append(team) + else: + blu_alliance.append(team) + team_alliance = team_alliance or (team['teamNumber'] == team_num and alliance) + + if not team_alliance: + continue + red_fmt = [] + blu_fmt = [] + for team in red_alliance: + red_fmt.append(FTCEventsClient.team_fmt(team, team_num=team_num)) + for team in blu_alliance: + blu_fmt.append(FTCEventsClient.team_fmt(team, team_num=team_num)) + + red_fmt = ", ".join(red_fmt) + blu_fmt = ", ".join(blu_fmt) + red_score = m['scoreRedFinal'] or "0" + blu_score = m['scoreBlueFinal'] or "0" + + wincode = "⬜" + if m['redWins']: + red_fmt = f"**{red_fmt}**" + red_score = f"**{red_score}**" + wincode = "🟥" + if m['blueWins']: + blu_fmt = f"**{blu_fmt}**" + blu_score = f"**{blu_score}**" + wincode = "🟦" + + field_desc = f"{red_fmt} vs. {blu_fmt}" + + if not played: + field_title = f"{m['description']}: unplayed" + embed.add_field(name=field_title, value=field_desc, inline=False) + continue + else: + if (m['redWins'] and team_alliance == "red") or (m['blueWins'] and team_alliance == 'blue'): + field_title = f"{m['description']}: Win 🟨" + elif not m['redWins'] and not m['blueWins']: + field_title = f"{m['description']}: Tie ⬜" + else: + field_title = f"{m['description']}: Loss 🇱" + if team_alliance == "red": + red_score = f"__{red_score}__" + else: + blu_score = f"__{blu_score}__" + + field_desc = field_desc + f" {wincode} {red_score}-{blu_score}" + embed.add_field(name=field_title, value=f"[{field_desc}]({FTCEventsClient.get_url_for_match(szn, ecode, m)})", inline=False) + class FTCInfo(Cog): @@ -93,24 +194,25 @@ async def team(self, ctx: DozerContext, team_num: int): await ctx.send("Invalid team number specified!") res = await self.ftcevents.req("teams?" + urlencode({'teamNumber': str(team_num)})) async with res: - print(res.status) if res.status == 400: await ctx.send("This team either did not compete this season, or it does not exist!") return - team_data = (await res.json(content_type=None))['teams'][0] - - print(team_data) + team_data = (await res.json(content_type=None)) + if not team_data: + await ctx.send(f"FTC-Events returned nothing on request with HTTP response code {res.status}.") + return + team_data = team_data['teams'][0] # many team entries lack a valid url - website = (team_data.get('website', "")).strip() + website = get_none_strip(team_data, 'website') if website and not (website.startswith("http://") or website.startswith("https://")): website = "http://" + website e = discord.Embed(color=embed_color, title=f'FIRST® Tech Challenge Team {team_num}', - url=f"https://ftc-events.firstinspires.org/{self.ftcevents.get_season()}/team/{team_num}") - e.add_field(name='Name', value=team_data.get('nameShort', "").strip() or "_ _") - e.add_field(name='Rookie Year', value=team_data.get('rookieYear', "Unknown")) + url=f"https://ftc-events.firstinspires.org/{FTCEventsClient.get_season()}/team/{team_num}") + e.add_field(name='Name', value=get_none_strip(team_data, 'nameShort') or "_ _") + e.add_field(name='Rookie Year', value=get_none_strip(team_data, 'rookieYear') or "Unknown") e.add_field(name='Location', value=', '.join((team_data['city'], team_data['stateProv'], team_data['country'])) or "Unknown") e.add_field(name='Org/Sponsors', value=team_data.get('nameFull', "").strip() or "_ _") @@ -121,6 +223,109 @@ async def team(self, ctx: DozerContext, team_num: int): `{prefix}ftc team 7244` - show information on team 7244, Out of the Box Robotics """ + @ftc.command() + @bot_has_permissions(embed_links=True) + async def matches(self, ctx: DozerContext, team_num: int, event_name: str = "latest"): + """Get a match schedule, defaulting to the latest listed event on FTC-Events""" + szn = FTCEventsClient.get_season() + req = await self.ftcevents.req("events?" + urlencode({'teamNumber': str(team_num)})) + events = [] + async with req: + if req.status == 400: + await ctx.send("This team either did not compete this season, or it does not exist!") + return + elif req.status > 400: + await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") + return + events = (await req.json())['events'] + + if len(events) == 0: + await ctx.send("This team did not attend any events this season!") + return + + event = None + if event_name == "latest": + # sort all events by date start + events = sorted(events, key=lambda e: FTCEventsClient.date_parse(e['dateStart']), reverse=True) + event = events[0] + + divisions = {} + # the event that should be used is the latest unpublished event. divisioned events are considered 1 event. + + # event division filter: + for e in events: + if e['divisionCode']: + if e['divisionCode'] not in divisions: + divisions[e['divisionCode']] = [e['code']] + else: + divisions[e['divisionCode']].append(e['code']) + + for e in events: + if e['code'] in divisions: + continue + event = e + break + + else: + for e in events: + if e['code'] == event_name: + event = e + if event is None: + await ctx.send(f"Team {team_num} did not attend {event_name}!") + return + print(f"Event for {team_num}:") + pprint.pprint(event) + # + event_url = f"https://ftc-events.firstinspires.org/{szn}/{event['code']}" + + # fetch the rankings + req = await self.ftcevents.req(f"rankings/{event['code']}?" + urlencode({'teamNumber': str(team_num)})) + async with req: + if req.status == 400: + await ctx.send(f"This team somehow competed at an event ({event_url}) that it is not ranked in -- did it no show?") + return + elif req.status > 400: + await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") + return + rank_res = (await req.json())['Rankings'] + + if not rank_res: + rank = None + description = "_Match schedule has not been published yet._" + else: + rank = rank_res[0] + description = f"Rank **{rank['rank']}**\nWLT **{rank['wins']}-{rank['losses']}-{rank['ties']}**\n"\ + f"QP/TBP1 **{rank['sortOrder1']} / {rank['sortOrder2']}** " + + embed = discord.Embed(color=embed_color, title=f"FTC Team {team_num} @ {event['name']}", url=event_url, description=description) + if not rank: + await ctx.send(embed=embed) + return + + # fetch the quals match schedule + req = await self.ftcevents.req(f"schedule/{event['code']}/qual/hybrid") + async with req: + if req.status >= 400: + await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") + return + FTCEventsClient.add_schedule_to_embed(embed, (await req.json())['schedule'], team_num, szn, event['code']) + + # fetch the playoffs match schedule + req = await self.ftcevents.req(f"schedule/{event['code']}/playoff/hybrid") + async with req: + if req.status >= 400: + await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") + return + FTCEventsClient.add_schedule_to_embed(embed, (await req.json())['schedule'], team_num, szn, event['code']) + + await ctx.send(embed=embed) + + matches.example_usage = """ + `{prefix}ftc matches 16377` - show matches for the latest event by team 16377, Spicy Ketchup + `{prefix}ftc matches 8393 USPACMP` - show matches for the Pennsylvania championship by team 8393, BrainSTEM + """ + + async def setup(bot): """Adds the FTC information cog to the bot.""" From f556e9d15e876e5bb724550b758108f5af7a6dd2 Mon Sep 17 00:00:00 2001 From: Guinea Wheek Date: Tue, 18 Apr 2023 18:05:31 -0700 Subject: [PATCH 03/66] pylint refactor --- dozer/cogs/ftc.py | 100 +++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/dozer/cogs/ftc.py b/dozer/cogs/ftc.py index 41f47480..6d1f7a6d 100755 --- a/dozer/cogs/ftc.py +++ b/dozer/cogs/ftc.py @@ -9,7 +9,6 @@ import aiohttp import async_timeout import discord -import pprint from discord.ext import commands from discord.utils import escape_markdown @@ -58,6 +57,20 @@ async def req(self, endpoint, season=None): tries += 1 if tries > 3: raise + + async def reqjson(self, endpoint, season=None, on_400=None, on_other=None): + """Reqjson.""" + res = await self.req(endpoint, season=season) + async with res: + if res.status == 400 and on_400: + await on_400(res) + return None + elif res.status >= 400: + if on_other: + await on_other(res) + return None + return await res.json(content_type=None) + @staticmethod def get_season(): @@ -197,7 +210,7 @@ async def team(self, ctx: DozerContext, team_num: int): if res.status == 400: await ctx.send("This team either did not compete this season, or it does not exist!") return - team_data = (await res.json(content_type=None)) + team_data = await res.json(content_type=None) if not team_data: await ctx.send(f"FTC-Events returned nothing on request with HTTP response code {res.status}.") return @@ -228,17 +241,13 @@ async def team(self, ctx: DozerContext, team_num: int): async def matches(self, ctx: DozerContext, team_num: int, event_name: str = "latest"): """Get a match schedule, defaulting to the latest listed event on FTC-Events""" szn = FTCEventsClient.get_season() - req = await self.ftcevents.req("events?" + urlencode({'teamNumber': str(team_num)})) - events = [] - async with req: - if req.status == 400: - await ctx.send("This team either did not compete this season, or it does not exist!") - return - elif req.status > 400: - await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") - return - events = (await req.json())['events'] + events = await self.ftcevents.reqjson("events?" + urlencode({'teamNumber': str(team_num)}), + on_400=lambda r: ctx.send("This team either did not compete this season, or it does not exist!"), + on_other=lambda r: ctx.send(f"FTC-Events returned an HTTP error status of: {r.status}. Something is broken.")) + if events is None: + return + events = events['events'] if len(events) == 0: await ctx.send("This team did not attend any events this season!") return @@ -273,50 +282,51 @@ async def matches(self, ctx: DozerContext, team_num: int, event_name: str = "lat if event is None: await ctx.send(f"Team {team_num} did not attend {event_name}!") return - print(f"Event for {team_num}:") - pprint.pprint(event) # event_url = f"https://ftc-events.firstinspires.org/{szn}/{event['code']}" # fetch the rankings - req = await self.ftcevents.req(f"rankings/{event['code']}?" + urlencode({'teamNumber': str(team_num)})) - async with req: - if req.status == 400: - await ctx.send(f"This team somehow competed at an event ({event_url}) that it is not ranked in -- did it no show?") - return - elif req.status > 400: - await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") - return - rank_res = (await req.json())['Rankings'] + rank_res = await self.ftcevents.reqjson(f"rankings/{event['code']}?" + urlencode({'teamNumber': str(team_num)}), + on_400=lambda r: ctx.send(f"This team somehow competed at an event ({event_url}) that it is not ranked in -- did it no show?"), + on_other=lambda r: ctx.send(f"FTC-Events returned an HTTP error status of: {r.status}. Something is broken.") + ) + if rank_res is None: + return + rank_res = rank_res['Rankings'] - if not rank_res: - rank = None - description = "_Match schedule has not been published yet._" - else: - rank = rank_res[0] - description = f"Rank **{rank['rank']}**\nWLT **{rank['wins']}-{rank['losses']}-{rank['ties']}**\n"\ - f"QP/TBP1 **{rank['sortOrder1']} / {rank['sortOrder2']}** " + if not rank_res: + rank = None + description = "_No rankings are available for this event._" + else: + rank = rank_res[0] + description = f"Rank **{rank['rank']}**\nWLT **{rank['wins']}-{rank['losses']}-{rank['ties']}**\n"\ + f"QP/TBP1 **{rank['sortOrder1']} / {rank['sortOrder2']}** " embed = discord.Embed(color=embed_color, title=f"FTC Team {team_num} @ {event['name']}", url=event_url, description=description) - if not rank: - await ctx.send(embed=embed) - return + has_matches_at_all = False # fetch the quals match schedule - req = await self.ftcevents.req(f"schedule/{event['code']}/qual/hybrid") - async with req: - if req.status >= 400: - await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") - return - FTCEventsClient.add_schedule_to_embed(embed, (await req.json())['schedule'], team_num, szn, event['code']) + req = await self.ftcevents.reqjson(f"schedule/{event['code']}/qual/hybrid", + on_other=lambda r: ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.")) + + if req is None: + return + res = req['schedule'] + has_matches_at_all = has_matches_at_all or bool(res) + FTCEventsClient.add_schedule_to_embed(embed, res, team_num, szn, event['code']) # fetch the playoffs match schedule - req = await self.ftcevents.req(f"schedule/{event['code']}/playoff/hybrid") - async with req: - if req.status >= 400: - await ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.") - return - FTCEventsClient.add_schedule_to_embed(embed, (await req.json())['schedule'], team_num, szn, event['code']) + req = await self.ftcevents.reqjson(f"schedule/{event['code']}/playoff/hybrid", + on_other=lambda r: ctx.send(f"FTC-Events returned an HTTP error status of: {req.status}. Something is broken.")) + + if req is None: + return + res = req['schedule'] + has_matches_at_all = has_matches_at_all or bool(res) + FTCEventsClient.add_schedule_to_embed(embed, res, team_num, szn, event['code']) + + if not has_matches_at_all: + embed.description = "_No match schedule is available yet._" await ctx.send(embed=embed) From c24110ba05741ff6f2062044d9e2a5b778369da3 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Thu, 20 Apr 2023 21:45:54 -0500 Subject: [PATCH 04/66] Create ftcqa.py Added the FTC Q/A forum cog from old dozer, with the dozer2.0 updates. --- ftcqa.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 ftcqa.py diff --git a/ftcqa.py b/ftcqa.py new file mode 100644 index 00000000..9175a1d2 --- /dev/null +++ b/ftcqa.py @@ -0,0 +1,77 @@ +"""Provides commands that pull information from First Q&A Form.""" +import discord +import aiohttp + +from bs4 import BeautifulSoup +from discord.ext import commands + +from context import DozerContext +from ._utils import * +from discord import app_commands + + +class QA(commands.Cog): + """QA commands""" + + def __init__(self, bot) -> None: + self.ses = aiohttp.ClientSession() + super().__init__() + self.bot = bot + + @commands.hybrid_command(name="qa", aliases=["ftcqa", "ftcqaforum", "qaforum"], pass_context=True) + @bot_has_permissions(embed_links = True) + @app_commands.describe(question = "The number of the question you want to look up") + async def qa(self, ctx: DozerContext, question: int): + """ + Shows Answers from the FTC Q&A + """ + async with self.ses.get('https://ftc-qa.firstinspires.org/onepage.html') as response: + html_data = await response.text() + + answers = BeautifulSoup(html_data, 'html.parser').get_text() + + start = answers.find('Q' + str(question) + ' ') + a = "" + if start > 0: + + finish = answers.find('answered', start) + 24 + a = answers[start:finish] + + # remove newlines + a = a.replace("\n", " ") + + # remove multiple spaces + a = " ".join(a.split()) + + embed = discord.Embed( + title = a[:a.find(" Q: ")], + url = "https://ftc-qa.firstinspires.org/qa/" + str(question), + color = discord.Color.blue()) + + embed.add_field(name = "Question", + value = a[a.find(" Q: ") + 1:a.find(" A: ")], + inline = False) + embed.add_field(name = "Answer", + value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline = False) + + embed.set_footer( + text = a[a.find(" ( Asked by ") + 1:]) + + await ctx.send(embed = embed, ephemeral = True) + + else: + a = "That question was not answered or does not exist." + + # add url + a += "\nhttps://ftc-qa.firstinspires.org/qa/" + question + await ctx.send(a, ephemeral = True) + + qa.example_usage = """ + `{prefix}qa 19` - show information on FTC Q&A #19 + """ + + +async def setup(bot): + """Adds the QA cog to the bot.""" + await bot.add_cog(QA(bot)) From 07a926cebead19dfcadd3f434458cec3354fe6ed Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Thu, 20 Apr 2023 21:52:20 -0500 Subject: [PATCH 05/66] Create profile_menus.py profile and message menus let users right click on a user and see server-related info, just like %/& user or profile commands in the past. --- dozer/cogs/profile_menus.py | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 dozer/cogs/profile_menus.py diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py new file mode 100644 index 00000000..1933fb8b --- /dev/null +++ b/dozer/cogs/profile_menus.py @@ -0,0 +1,54 @@ +import discord +from discord.ext import commands +from discord import app_commands + + +class ProfileMenus(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.ctx_menu = app_commands.ContextMenu( + name = 'View Profile', + callback = self.profile, # sets the callback the view_profile function + ) + self.bot.tree.add_command(self.ctx_menu) # add the context menu to the tree + + async def cog_unload(self) -> None: + self.bot.tree.remove_command(self.ctx_menu.name, type = self.ctx_menu.type) + + async def profile(self, interaction: discord.Interaction, member: discord.Member): + # await interaction.response.send_message(f'{member} joined at {discord.utils.format_dt(member.joined_at)}', + # ephemeral = False) # temp false for testing + if member is None: + member = interaction.user + + icon_url = member_avatar_url(member) + + embed = discord.Embed(title = member.display_name, description = f'{member!s} ({member.id}) | {member.mention}', + color = member.color) + embed.add_field(name = 'Bot Created' if member.bot else 'Account Created', + value = discord.utils.format_dt(member.created_at), inline = True) + embed.add_field(name = 'Member Joined', value = discord.utils.format_dt(member.joined_at), inline = True) + if member.premium_since is not None: + embed.add_field(name = 'Member Boosted', value = discord.utils.format_dt(member.premium_since), + inline = True) + if len(member.roles) > 1: + role_string = ' '.join([r.mention for r in member.roles][1:]) + else: + role_string = member.roles[0].mention + s = "s" if len(member.roles) >= 2 else "" + embed.add_field(name = f"Role{s}: ", value = role_string, inline = False) + embed.set_thumbnail(url = icon_url) + await interaction.response.send_message(embed = embed, ephemeral = True) + + +async def setup(bot): + """Adds the profile context menus cog to the bot.""" + await bot.add_cog(ProfileMenus(bot)) + + +def member_avatar_url(m: discord.Member, static_format = 'png', size = 32): + """return avatar url""" + if m.avatar is not None: + return m.avatar.replace(static_format = static_format, size = size) + else: + return None From 2471d638451ed2fd6c9c8c6e81e62f40bb7f449b Mon Sep 17 00:00:00 2001 From: JayFromProgramming Date: Mon, 1 May 2023 13:27:32 -0400 Subject: [PATCH 06/66] Slight change to modmail subject to long behavior Signed-off-by: JayFromProgramming --- dozer/cogs/moderation.py | 45 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/dozer/cogs/moderation.py b/dozer/cogs/moderation.py index 29d04b68..10ce4cca 100755 --- a/dozer/cogs/moderation.py +++ b/dozer/cogs/moderation.py @@ -177,16 +177,23 @@ async def start_punishment_timers(self): except discord.NotFound: logger.warning(f"Guild {r.guild_id} not found, skipping punishment timer") continue - actor = guild.get_member(r.actor_id) - target = guild.get_member(r.target_id) + try: + actor = await guild.fetch_member(r.actor_id) + except discord.NotFound: + actor = None + try: + target = await guild.fetch_member(r.target_id) + except discord.NotFound: + logger.warning(f"Target {r.target_id} not found, skipping punishment timer") + continue orig_channel = self.bot.get_channel(r.orig_channel_id) punishment_type = r.type_of_punishment reason = r.reason or "" seconds = max(int(r.target_ts - time.time()), 0.01) - await PunishmentTimerRecords.delete(id=r.id) + # await PunishmentTimerRecords.delete(id=r.id) self.bot.loop.create_task( self.punishment_timer(seconds, target, PunishmentTimerRecords.type_map[punishment_type], reason, actor, - orig_channel)) + orig_channel, timer_id=r.id)) logger.info( f"Restarted {PunishmentTimerRecords.type_map[punishment_type].__name__} of {target} in {guild}") @@ -203,7 +210,7 @@ async def restart_all_timers(self): async def punishment_timer(self, seconds: int, target: discord.Member, punishment, reason: str, actor: discord.Member, orig_channel=None, - global_modlog: bool = True): + global_modlog: bool = True, timer_id: int = None): """Asynchronous task that sleeps for a set time to unmute/undeafen a member for a set period of time.""" # Add this task to the list of active timer tasks @@ -216,18 +223,22 @@ async def punishment_timer(self, seconds: int, target: discord.Member, punishmen if seconds == 0: return - # register the timer - ent = PunishmentTimerRecords( - guild_id=target.guild.id, - actor_id=actor.id, - target_id=target.id, - orig_channel_id=orig_channel.id if orig_channel else 0, - type_of_punishment=punishment.type, - reason=reason, - target_ts=int(seconds + time.time()), - self_inflicted=not global_modlog - ) - await ent.update_or_add() + if timer_id is None: + # register the timer + ent = PunishmentTimerRecords( + guild_id=target.guild.id, + actor_id=actor.id, + target_id=target.id, + orig_channel_id=orig_channel.id if orig_channel else 0, + type_of_punishment=punishment.type, + reason=reason, + target_ts=int(seconds + time.time()), + self_inflicted=not global_modlog + ) + await ent.update_or_add() + else: + ent = (await PunishmentTimerRecords.get_by(id=timer_id))[0] + seconds = max(int(ent.target_ts - time.time()), 0.01) await asyncio.sleep(seconds) From 9d205a0c64d32c7d49fb82a2b53da248fbc42aaf Mon Sep 17 00:00:00 2001 From: JayFromProgramming Date: Mon, 1 May 2023 13:27:59 -0400 Subject: [PATCH 07/66] Revert "Slight change to modmail subject to long behavior" This reverts commit 2471d638451ed2fd6c9c8c6e81e62f40bb7f449b. --- dozer/cogs/moderation.py | 45 +++++++++++++++------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/dozer/cogs/moderation.py b/dozer/cogs/moderation.py index 10ce4cca..29d04b68 100755 --- a/dozer/cogs/moderation.py +++ b/dozer/cogs/moderation.py @@ -177,23 +177,16 @@ async def start_punishment_timers(self): except discord.NotFound: logger.warning(f"Guild {r.guild_id} not found, skipping punishment timer") continue - try: - actor = await guild.fetch_member(r.actor_id) - except discord.NotFound: - actor = None - try: - target = await guild.fetch_member(r.target_id) - except discord.NotFound: - logger.warning(f"Target {r.target_id} not found, skipping punishment timer") - continue + actor = guild.get_member(r.actor_id) + target = guild.get_member(r.target_id) orig_channel = self.bot.get_channel(r.orig_channel_id) punishment_type = r.type_of_punishment reason = r.reason or "" seconds = max(int(r.target_ts - time.time()), 0.01) - # await PunishmentTimerRecords.delete(id=r.id) + await PunishmentTimerRecords.delete(id=r.id) self.bot.loop.create_task( self.punishment_timer(seconds, target, PunishmentTimerRecords.type_map[punishment_type], reason, actor, - orig_channel, timer_id=r.id)) + orig_channel)) logger.info( f"Restarted {PunishmentTimerRecords.type_map[punishment_type].__name__} of {target} in {guild}") @@ -210,7 +203,7 @@ async def restart_all_timers(self): async def punishment_timer(self, seconds: int, target: discord.Member, punishment, reason: str, actor: discord.Member, orig_channel=None, - global_modlog: bool = True, timer_id: int = None): + global_modlog: bool = True): """Asynchronous task that sleeps for a set time to unmute/undeafen a member for a set period of time.""" # Add this task to the list of active timer tasks @@ -223,22 +216,18 @@ async def punishment_timer(self, seconds: int, target: discord.Member, punishmen if seconds == 0: return - if timer_id is None: - # register the timer - ent = PunishmentTimerRecords( - guild_id=target.guild.id, - actor_id=actor.id, - target_id=target.id, - orig_channel_id=orig_channel.id if orig_channel else 0, - type_of_punishment=punishment.type, - reason=reason, - target_ts=int(seconds + time.time()), - self_inflicted=not global_modlog - ) - await ent.update_or_add() - else: - ent = (await PunishmentTimerRecords.get_by(id=timer_id))[0] - seconds = max(int(ent.target_ts - time.time()), 0.01) + # register the timer + ent = PunishmentTimerRecords( + guild_id=target.guild.id, + actor_id=actor.id, + target_id=target.id, + orig_channel_id=orig_channel.id if orig_channel else 0, + type_of_punishment=punishment.type, + reason=reason, + target_ts=int(seconds + time.time()), + self_inflicted=not global_modlog + ) + await ent.update_or_add() await asyncio.sleep(seconds) From 604396f8919fbb873fd2a0357cf8dbc4af78df1e Mon Sep 17 00:00:00 2001 From: JayFromProgramming Date: Mon, 1 May 2023 13:28:19 -0400 Subject: [PATCH 08/66] Fixed punishment timer restart issue Signed-off-by: JayFromProgramming --- dozer/cogs/moderation.py | 45 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/dozer/cogs/moderation.py b/dozer/cogs/moderation.py index 29d04b68..10ce4cca 100755 --- a/dozer/cogs/moderation.py +++ b/dozer/cogs/moderation.py @@ -177,16 +177,23 @@ async def start_punishment_timers(self): except discord.NotFound: logger.warning(f"Guild {r.guild_id} not found, skipping punishment timer") continue - actor = guild.get_member(r.actor_id) - target = guild.get_member(r.target_id) + try: + actor = await guild.fetch_member(r.actor_id) + except discord.NotFound: + actor = None + try: + target = await guild.fetch_member(r.target_id) + except discord.NotFound: + logger.warning(f"Target {r.target_id} not found, skipping punishment timer") + continue orig_channel = self.bot.get_channel(r.orig_channel_id) punishment_type = r.type_of_punishment reason = r.reason or "" seconds = max(int(r.target_ts - time.time()), 0.01) - await PunishmentTimerRecords.delete(id=r.id) + # await PunishmentTimerRecords.delete(id=r.id) self.bot.loop.create_task( self.punishment_timer(seconds, target, PunishmentTimerRecords.type_map[punishment_type], reason, actor, - orig_channel)) + orig_channel, timer_id=r.id)) logger.info( f"Restarted {PunishmentTimerRecords.type_map[punishment_type].__name__} of {target} in {guild}") @@ -203,7 +210,7 @@ async def restart_all_timers(self): async def punishment_timer(self, seconds: int, target: discord.Member, punishment, reason: str, actor: discord.Member, orig_channel=None, - global_modlog: bool = True): + global_modlog: bool = True, timer_id: int = None): """Asynchronous task that sleeps for a set time to unmute/undeafen a member for a set period of time.""" # Add this task to the list of active timer tasks @@ -216,18 +223,22 @@ async def punishment_timer(self, seconds: int, target: discord.Member, punishmen if seconds == 0: return - # register the timer - ent = PunishmentTimerRecords( - guild_id=target.guild.id, - actor_id=actor.id, - target_id=target.id, - orig_channel_id=orig_channel.id if orig_channel else 0, - type_of_punishment=punishment.type, - reason=reason, - target_ts=int(seconds + time.time()), - self_inflicted=not global_modlog - ) - await ent.update_or_add() + if timer_id is None: + # register the timer + ent = PunishmentTimerRecords( + guild_id=target.guild.id, + actor_id=actor.id, + target_id=target.id, + orig_channel_id=orig_channel.id if orig_channel else 0, + type_of_punishment=punishment.type, + reason=reason, + target_ts=int(seconds + time.time()), + self_inflicted=not global_modlog + ) + await ent.update_or_add() + else: + ent = (await PunishmentTimerRecords.get_by(id=timer_id))[0] + seconds = max(int(ent.target_ts - time.time()), 0.01) await asyncio.sleep(seconds) From a74bb29e2e1bd2d82cb0ef34c3494efb9dd00566 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 1 May 2023 16:15:12 -0500 Subject: [PATCH 09/66] Update profile_menus.py Added comments for pylint --- dozer/cogs/profile_menus.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py index 1933fb8b..c4572c44 100644 --- a/dozer/cogs/profile_menus.py +++ b/dozer/cogs/profile_menus.py @@ -1,9 +1,13 @@ +"""Provides the ability to add commands to user profiles in servers""" import discord from discord.ext import commands from discord import app_commands class ProfileMenus(commands.Cog): + """ + Creates a profile menu object for the bot + """ def __init__(self, bot: commands.Bot) -> None: self.bot = bot self.ctx_menu = app_commands.ContextMenu( @@ -16,6 +20,7 @@ async def cog_unload(self) -> None: self.bot.tree.remove_command(self.ctx_menu.name, type = self.ctx_menu.type) async def profile(self, interaction: discord.Interaction, member: discord.Member): + """Creates the ephemeral response that will be sent to the user when they interact with the 'View Profile' button""" # await interaction.response.send_message(f'{member} joined at {discord.utils.format_dt(member.joined_at)}', # ephemeral = False) # temp false for testing if member is None: From 929a1ac2006276dd772f644cd632f6dc8c627695 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Tue, 2 May 2023 00:16:43 -0500 Subject: [PATCH 10/66] Update ftcqa.py Added frc qa command See https://imgur.com/a/ipDtoVw for both commands working --- ftcqa.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/ftcqa.py b/ftcqa.py index 9175a1d2..94d9438c 100644 --- a/ftcqa.py +++ b/ftcqa.py @@ -18,10 +18,10 @@ def __init__(self, bot) -> None: super().__init__() self.bot = bot - @commands.hybrid_command(name="qa", aliases=["ftcqa", "ftcqaforum", "qaforum"], pass_context=True) + @commands.hybrid_command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) @bot_has_permissions(embed_links = True) @app_commands.describe(question = "The number of the question you want to look up") - async def qa(self, ctx: DozerContext, question: int): + async def ftcqa(self, ctx: DozerContext, question: int): """ Shows Answers from the FTC Q&A """ @@ -67,10 +67,62 @@ async def qa(self, ctx: DozerContext, question: int): a += "\nhttps://ftc-qa.firstinspires.org/qa/" + question await ctx.send(a, ephemeral = True) - qa.example_usage = """ - `{prefix}qa 19` - show information on FTC Q&A #19 + ftcqa.example_usage = """ + `{prefix}ftcqa 19` - show information on FTC Q&A #19 """ + @commands.hybrid_command(name="frcqa", aliases=["frcqaforum"], pass_context=True) + @bot_has_permissions(embed_links = True) + @app_commands.describe(question = "The number of the question you want to look up") + async def frcqa(self, ctx: DozerContext, question: int): + """ + Shows Answers from the FTC Q&A + """ + async with self.ses.get('https://frc-qa.firstinspires.org/onepage.html') as response: + html_data = await response.text() + + answers = BeautifulSoup(html_data, 'html.parser').get_text() + + start = answers.find('Q' + str(question) + ' ') + a = "" + if start > 0: + + finish = answers.find('answered', start) + 24 + a = answers[start:finish] + + # remove newlines + a = a.replace("\n", " ") + + # remove multiple spaces + a = " ".join(a.split()) + + embed = discord.Embed( + title = a[:a.find(" Q: ")], + url = "https://frc-qa.firstinspires.org/qa/" + str(question), + color = discord.Color.blue()) + + embed.add_field(name = "Question", + value = a[a.find(" Q: ") + 1:a.find(" A: ")], + inline = False) + embed.add_field(name = "Answer", + value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline = False) + + embed.set_footer( + text = a[a.find(" ( Asked by ") + 1:]) + + await ctx.send(embed = embed, ephemeral = True) + + else: + a = "That question was not answered or does not exist." + + # add url + a += "\nhttps://frc-qa.firstinspires.org/qa/" + question + await ctx.send(a, ephemeral = True) + + frcqa.example_usage = """ + `{prefix}frcqa 19` - show information on FRC Q&A #19 + """ async def setup(bot): """Adds the QA cog to the bot.""" From 6d9c692d9ca8e938378a1252f6c1ebb24d62e1f0 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Fri, 5 May 2023 23:33:26 -0500 Subject: [PATCH 11/66] Update profile_menus.py Fixed up icon_url to devyn's suggestion, removing the extra helper function. --- dozer/cogs/profile_menus.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py index c4572c44..8e919864 100644 --- a/dozer/cogs/profile_menus.py +++ b/dozer/cogs/profile_menus.py @@ -26,7 +26,7 @@ async def profile(self, interaction: discord.Interaction, member: discord.Member if member is None: member = interaction.user - icon_url = member_avatar_url(member) + icon_url = member.avatar.replace(static_format = 'png', size = 32) or None embed = discord.Embed(title = member.display_name, description = f'{member!s} ({member.id}) | {member.mention}', color = member.color) @@ -49,11 +49,3 @@ async def profile(self, interaction: discord.Interaction, member: discord.Member async def setup(bot): """Adds the profile context menus cog to the bot.""" await bot.add_cog(ProfileMenus(bot)) - - -def member_avatar_url(m: discord.Member, static_format = 'png', size = 32): - """return avatar url""" - if m.avatar is not None: - return m.avatar.replace(static_format = static_format, size = size) - else: - return None From 274c8eafc86a5bb33b4c32830082a07482116889 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sat, 6 May 2023 01:14:12 -0500 Subject: [PATCH 12/66] Update toa.py Just cleaned up the command a bit, adding an ephemeral response for when slash is used instead of the regular text command. Also removed the toa group, as it's not necessary when there's only the one toa team command command is now just toateam/toa/ftcteam/ftcteaminfo --- dozer/cogs/toa.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/dozer/cogs/toa.py b/dozer/cogs/toa.py index 4e0cab04..a7ce39f3 100755 --- a/dozer/cogs/toa.py +++ b/dozer/cogs/toa.py @@ -8,10 +8,10 @@ import aiohttp import async_timeout import discord +from discord import app_commands from discord.ext import commands -from discord.utils import escape_markdown -from dozer.context import DozerContext +from context import DozerContext from ._utils import * embed_color = discord.Color(0xf89808) @@ -56,28 +56,17 @@ async def req(self, endpoint): raise -class TOA(Cog): +class TOA(commands.Cog): """TOA commands""" def __init__(self, bot: commands.Bot): - super().__init__(bot) + super().__init__() self.http_session = bot.add_aiohttp_ses(aiohttp.ClientSession()) self.parser = TOAParser(bot.config['toa']['key'], self.http_session, app_name=bot.config['toa']['app_name']) - @group(invoke_without_command=True) - async def toa(self, ctx: DozerContext, team_num: int): - """ - Get FTC-related information from The Orange Alliance. - If no subcommand is specified, the `team` subcommand is inferred, and the argument is taken as a team number. - """ - await self.team.callback(self, ctx, team_num) # This works but Pylint throws an error - - toa.example_usage = """ - `{prefix}toa 5667` - show information on team 5667, Robominers - """ - - @toa.command() + @commands.hybrid_command(name="toateam", aliases=["toa", "ftcteam", "ftcteaminfo"]) @bot_has_permissions(embed_links=True) + @app_commands.describe(team_num="The team you want to see toa info about") async def team(self, ctx: DozerContext, team_num: int): """Get information on an FTC team by number.""" res = json.loads(await self.parser.req("team/" + str(team_num))) @@ -96,8 +85,7 @@ async def team(self, ctx: DozerContext, team_num: int): value=', '.join((team_data['city'], team_data['state_prov'], team_data['country']))) e.add_field(name='Website', value=team_data['website'] or 'n/a') e.add_field(name='Team Info Page', value=f'https://theorangealliance.org/teams/{team_data["team_key"]}') - e.set_footer(text='Triggered by ' + escape_markdown(ctx.author.display_name)) - await ctx.send('', embed=e) + await ctx.send('', embed=e, ephemeral=True) team.example_usage = """ `{prefix}toa team 12670` - show information on team 12670, Eclipse From f3519f3f6645123f119144f618d86a7c69bedfd0 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sat, 6 May 2023 01:21:00 -0500 Subject: [PATCH 13/66] Update info.py Added (or returned, it was in the old ftc dozer that we were using as a base for dozer2.0) Added role/rolemembers commands, that show users with role and info about specific roles Updated guild command Updated profile/member command to be ephemeral with slash --- dozer/cogs/info.py | 82 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/dozer/cogs/info.py b/dozer/cogs/info.py index 2af84e4f..3316d708 100755 --- a/dozer/cogs/info.py +++ b/dozer/cogs/info.py @@ -74,7 +74,7 @@ async def member(self, ctx: DozerContext, *, member: discord.Member = None): embed.add_field(name='Roles', value=', '.join(role.mention for role in roles) or 'None', inline=False) footers.append(f"Color: {str(member.color).upper()}") embed.set_footer(text="; ".join(footers)) - await ctx.send(embed=embed) + await ctx.send(embed=embed, ephemeral=True) member.example_usage = """ `{prefix}member`: show your member info @@ -153,33 +153,79 @@ async def rolemembers(self, ctx: DozerContext, role: discord.Role): @guild_only() @cooldown(1, 10, BucketType.channel) - @command(aliases=['server', 'guildinfo', 'serverinfo']) + @commands.hybrid_command(name = "server", aliases = ['guild', 'guildinfo', 'serverinfo']) async def guild(self, ctx: DozerContext): """Retrieve information about this guild.""" guild = ctx.guild static_emoji = sum(not e.animated for e in ctx.guild.emojis) animated_emoji = sum(e.animated for e in ctx.guild.emojis) - embed = discord.Embed(title=f"Info for guild: {guild.name}", description=f"Members: {guild.member_count}", - color=blurple) - - embed.set_thumbnail(url=guild.icon.url if guild.icon is not None else None) - - embed.add_field(name='Created on', value=f"") - embed.add_field(name='Owner', value=guild.owner) - embed.add_field(name='Emoji', value=f"{static_emoji} static, {animated_emoji} animated") - embed.add_field(name='Roles', value=str(len(guild.roles) - 1)) # Remove @everyone - embed.add_field(name='Channels', value=str(len(guild.channels))) - embed.add_field(name='Nitro Boost Info', value=f'Level {ctx.guild.premium_tier}, ' - f'{ctx.guild.premium_subscription_count} booster(s), ' - f'{ctx.guild.filesize_limit // 1024 ** 2}MiB files, ' - f'{ctx.guild.bitrate_limit / 1000:0.1f}kbps voice') - - await ctx.send(embed=embed) + e = discord.Embed(color = blurple) + e.set_thumbnail(url = guild.icon.url if guild.icon else None) + e.title = guild.name + e.description = f"{guild.member_count} members, {len(guild.channels)} channels, {len(guild.roles) - 1} roles" + e.add_field(name = 'ID', value = guild.id) + e.add_field(name = 'Created on', value = discord.utils.format_dt(guild.created_at)) + e.add_field(name = 'Owner', value = guild.owner.mention) + e.add_field(name = 'Emoji', value = f"{static_emoji} static, {animated_emoji} animated") + e.add_field(name = 'Nitro Boost', value = f'Level {ctx.guild.premium_tier}, ' + f'{ctx.guild.premium_subscription_count} booster(s)\n' + f'{ctx.guild.filesize_limit // 1024**2}MiB files, ' + f'{ctx.guild.bitrate_limit / 1000:0.1f}kbps voice') + await ctx.send(embed = e, ephemeral = True) guild.example_usage = """ `{prefix}guild` - get information about this guild """ + @commands.hybrid_command() + async def stats(self, ctx: DozerContext): + """Get current running internal/host stats for the bot""" + info = await ctx.bot.application_info() + + frame = "\n".join( + map(lambda x: f"{str(x[0]):<24}{str(x[1])}", { + "Users:": len(ctx.bot.users), + "Channels:": len(list(ctx.bot.get_all_channels())), + "Servers:": len(ctx.bot.guilds), + "": "", + f"{' Host stats ':=^48}": "", + "Operating system:": os_name, + "Process uptime": str(datetime.timedelta(seconds = round(time.time() - startup_time))) + }.items())) + embed = discord.Embed(title = f"Stats for {info.name}", description = f"Bot owner: {info.owner.mention}```{frame}```", color = blurple) + await ctx.send(embed=embed, ephemeral = True) + + stats.example_usage = """ + `{prefix}stats` - get current bot/host stats + """ + + @commands.hybrid_command(aliases = ['roleinfo']) + @guild_only() + @cooldown(1, 10, BucketType.channel) + async def role(self, ctx: DozerContext, role: discord.Role): + """Retrieve info about a role in this guild""" + embed = discord.Embed(title = f"Info for role: {role.name}", description = f"{role.mention} ({role.id})", + color = role.color) + embed.add_field(name = "Created on", value = discord.utils.format_dt(role.created_at)) + embed.add_field(name = "Position", value = role.position) + embed.add_field(name = "Color", value = str(role.color).upper()) + embed.add_field(name = "Assigned members", value = f"{len(role.members)}", inline = False) + await ctx.send(embed = embed, ephemeral = True) + + @commands.hybrid_command(aliases = ['withrole']) + @guild_only() + async def rolemembers(self, ctx: DozerContext, role: discord.Role): + """Retrieve members who have this role""" + await ctx.defer(ephemeral = True) + embeds = [] + for page_num, page in enumerate(chunk(role.members, 10)): + embed = discord.Embed(title = f"Members for role: {role.name}", color = role.color) + embed.description = "\n".join(f"{member.mention}({member.id})" for member in page) + embed.set_footer(text = f"Page {page_num + 1} of {math.ceil(len(role.members) / 10)}") + embeds.append(embed) + await paginate(ctx, embeds) + + async def setup(bot): """Adds the info cog to the bot""" From 143c2f758ef1a99d5fac27928a57c1d17609b21a Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sat, 6 May 2023 01:27:39 -0500 Subject: [PATCH 14/66] Forgot to add the helper stuff for stats --- dozer/cogs/info.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dozer/cogs/info.py b/dozer/cogs/info.py index 3316d708..fa1d50ee 100755 --- a/dozer/cogs/info.py +++ b/dozer/cogs/info.py @@ -15,7 +15,21 @@ blurple = discord.Color.blurple() datetime_format = '%Y-%m-%d %H:%M:%S\nUTC' +startup_time = time.time() +try: + with open("/etc/os-release") as f: + os_name = re.findall(r'PRETTY_NAME=\"(.+?)\"', f.read())[0] +except Exception: + os_name = "Windows probably" + + +class Info(Cog): + """Commands for getting information about people and things on Discord.""" + + def __init__(self, bot): + super().__init__(bot) + self.bot = bot class Info(Cog): """Commands for getting information about people and things on Discord.""" From 616762f5afefca78701e9ab33135907f709359c3 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sat, 6 May 2023 01:33:32 -0500 Subject: [PATCH 15/66] Update toa.py differences in file structure on our version vs this one --- dozer/cogs/toa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dozer/cogs/toa.py b/dozer/cogs/toa.py index a7ce39f3..61b82e0c 100755 --- a/dozer/cogs/toa.py +++ b/dozer/cogs/toa.py @@ -11,7 +11,7 @@ from discord import app_commands from discord.ext import commands -from context import DozerContext +from dozer.context import DozerContext from ._utils import * embed_color = discord.Color(0xf89808) From b8d5b9ef17fb35d22b7ebef5cefb33551683d6d2 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sat, 6 May 2023 01:40:06 -0500 Subject: [PATCH 16/66] Update info.py Forgot to add imports that we had, not easy trying to merge stuff directly on github (thanks pylint) --- dozer/cogs/info.py | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/dozer/cogs/info.py b/dozer/cogs/info.py index fa1d50ee..7ade397b 100755 --- a/dozer/cogs/info.py +++ b/dozer/cogs/info.py @@ -3,6 +3,8 @@ import typing from datetime import timezone, datetime, date from difflib import SequenceMatcher +import time +import re import discord import humanize @@ -30,10 +32,7 @@ class Info(Cog): def __init__(self, bot): super().__init__(bot) self.bot = bot - -class Info(Cog): - """Commands for getting information about people and things on Discord.""" - + @command(aliases=['user', 'memberinfo', 'userinfo']) @guild_only() @bot_has_permissions(embed_links=True) @@ -213,32 +212,6 @@ async def stats(self, ctx: DozerContext): `{prefix}stats` - get current bot/host stats """ - @commands.hybrid_command(aliases = ['roleinfo']) - @guild_only() - @cooldown(1, 10, BucketType.channel) - async def role(self, ctx: DozerContext, role: discord.Role): - """Retrieve info about a role in this guild""" - embed = discord.Embed(title = f"Info for role: {role.name}", description = f"{role.mention} ({role.id})", - color = role.color) - embed.add_field(name = "Created on", value = discord.utils.format_dt(role.created_at)) - embed.add_field(name = "Position", value = role.position) - embed.add_field(name = "Color", value = str(role.color).upper()) - embed.add_field(name = "Assigned members", value = f"{len(role.members)}", inline = False) - await ctx.send(embed = embed, ephemeral = True) - - @commands.hybrid_command(aliases = ['withrole']) - @guild_only() - async def rolemembers(self, ctx: DozerContext, role: discord.Role): - """Retrieve members who have this role""" - await ctx.defer(ephemeral = True) - embeds = [] - for page_num, page in enumerate(chunk(role.members, 10)): - embed = discord.Embed(title = f"Members for role: {role.name}", color = role.color) - embed.description = "\n".join(f"{member.mention}({member.id})" for member in page) - embed.set_footer(text = f"Page {page_num + 1} of {math.ceil(len(role.members) / 10)}") - embeds.append(embed) - await paginate(ctx, embeds) - async def setup(bot): From a19aac1e8469dd1ae3682a325b7a4550a0d7565c Mon Sep 17 00:00:00 2001 From: JayFromProgramming Date: Wed, 10 May 2023 11:12:51 -0400 Subject: [PATCH 17/66] Finally fixed issue with punishment not un-punishing people Signed-off-by: JayFromProgramming --- dozer/cogs/moderation.py | 70 ++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/dozer/cogs/moderation.py b/dozer/cogs/moderation.py index 10ce4cca..5f1d2eaf 100755 --- a/dozer/cogs/moderation.py +++ b/dozer/cogs/moderation.py @@ -26,8 +26,6 @@ MAX_PURGE = 1000 - - class SafeRoleConverter(RoleConverter): """Allows for @everyone to be specified without pinging everyone""" @@ -118,6 +116,13 @@ async def mod_log(self, actor: discord.Member, action: str, target: Union[discor await orig_channel.send("Failed to DM modlog to user") else: logger.warning(f"Failed to DM modlog to user {target} ({target.id})") + except discord.HTTPException as e: + if orig_channel is not None: + await orig_channel.send(f"Failed to DM modlog to user: `{e}`") + else: + logger.warning(f"Failed to DM modlog to user {target} ({target.id}): {e}") + except AttributeError: + pass finally: modlog_embed.remove_field(2) modlog_channel = await GuildModLog.get_by(guild_id=actor.guild.id) if guild_override is None else \ @@ -140,16 +145,31 @@ async def mod_log(self, actor: discord.Member, action: str, target: Union[discor async def perm_override(self, member: discord.Member, **overwrites): """Applies the given overrides to the given member in their guild.""" - for channel in member.guild.channels: + logger.debug(f"Applying overrides to {member} ({member.id})") + overwrite_count = 0 + guild = await self.bot.fetch_guild(member.guild.id) + channels = await guild.fetch_channels() + # For some reason guild.me is returning None only sometimes, so this is a workaround to get perm_overrides working + me = await guild.fetch_member(self.bot.user.id) + for channel in channels: overwrite = channel.overwrites_for(member) - if channel.permissions_for(member.guild.me).manage_roles: + if channel.permissions_for(me).manage_roles: overwrite.update(**overwrites) try: await channel.set_permissions(target=member, overwrite=None if overwrite.is_empty() else overwrite) + overwrite_count += 1 except discord.Forbidden as e: logger.error( f"Failed to catch missing perms in {channel} ({channel.id}) Guild: {channel.guild.id}; Error: {e}") - + except discord.HTTPException as e: + logger.error( + f"Failed to catch discords horrid permissions system in " + f"{channel} ({channel.id}) Guild: {channel.guild.id}; Error: {e}") + except Exception as e: + logger.error(f"Failed to catch some unknown or unexpected error: {e}") + else: + logger.warning(f"Missing permissions to manage roles in {channel} ({channel.id})") + logger.debug(f"Applied {overwrite_count}/({len(channels)}) overrides to {member} ({member.id})") hm_regex = re.compile(r"((?P\d+)y)?((?P\d+)M)?((?P\d+)w)?((?P\d+)d)?((?P\d+)h)?((?P\d+)m)?((" r"?P\d+)s)?") @@ -238,25 +258,33 @@ async def punishment_timer(self, seconds: int, target: discord.Member, punishmen await ent.update_or_add() else: ent = (await PunishmentTimerRecords.get_by(id=timer_id))[0] - seconds = max(int(ent.target_ts - time.time()), 0.01) + # seconds = max(int(ent.target_ts - time.time()), 0.01) await asyncio.sleep(seconds) + logger.info(f"Finished{' self' if not global_modlog else ''} {punishment.__name__} " + f"timer of \"{target}\" in \"{target.guild}\", preforming un-punishment") user = await punishment.get_by(member_id=target.id) - if len(user) != 0: - await self.mod_log(actor=actor, - action="un" + punishment.past_participle, - target=target, - reason=reason, - orig_channel=orig_channel, - embed_color=discord.Color.green(), - global_modlog=global_modlog) - - self.punishment_timer_tasks.remove(asyncio.current_task()) - self.bot.loop.create_task(coro=punishment.finished_callback(self, target)) - if ent: - await PunishmentTimerRecords.delete(guild_id=target.guild.id, target_id=target.id, - type_of_punishment=punishment.type) + try: + if len(user) != 0: + await self.mod_log(actor=actor, + action="un" + punishment.past_participle, + target=target, + reason=reason, + orig_channel=orig_channel, + embed_color=discord.Color.green(), + global_modlog=global_modlog) + self.punishment_timer_tasks.remove(asyncio.current_task()) + self.bot.loop.create_task(coro=punishment.finished_callback(self, target)) + else: + logger.warning(f"User {target} was not found in the {punishment.__name__} database, skipping un-punishment") + + if ent: + await PunishmentTimerRecords.delete(guild_id=target.guild.id, target_id=target.id, + type_of_punishment=punishment.type) + except Exception as e: + logger.error(f"Error while un-punishing {target} in {target.guild}, {e}") + logger.exception(e) async def _check_links_warn(self, msg: discord.Message, role: discord.Role): """Warns a user that they can't send links.""" @@ -1338,7 +1366,7 @@ async def initial_create(cls): )""") def __init__(self, guild_id: int, actor_id: int, target_id: int, type_of_punishment: int, target_ts: int, - orig_channel_id: int = None, reason: str = None, input_id: int = None, self_inflicted: bool =False): + orig_channel_id: int = None, reason: str = None, input_id: int = None, self_inflicted: bool = False): super().__init__() self.id = input_id self.guild_id = guild_id From fd24e651e3ceff9bfae785d0c84d976868a28090 Mon Sep 17 00:00:00 2001 From: travis weir Date: Tue, 16 May 2023 21:07:04 -0500 Subject: [PATCH 18/66] 25 branches is fine --- pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index 9d342e7e..00c72328 100755 --- a/pylintrc +++ b/pylintrc @@ -442,7 +442,7 @@ max-attributes=25 max-bool-expr=5 # Maximum number of branch for function / method body -max-branches=20 +max-branches=25 # Maximum number of lines in a module max-module-lines=1500 From fb6f7eb18b659c820a5947b84a180465ca43da8d Mon Sep 17 00:00:00 2001 From: travis weir Date: Tue, 16 May 2023 21:18:52 -0500 Subject: [PATCH 19/66] Fix a little whoopsie --- dozer/Components/CustomJoinLeaveMessages.py | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dozer/Components/CustomJoinLeaveMessages.py b/dozer/Components/CustomJoinLeaveMessages.py index c6c683b2..f59f0499 100644 --- a/dozer/Components/CustomJoinLeaveMessages.py +++ b/dozer/Components/CustomJoinLeaveMessages.py @@ -31,8 +31,8 @@ def format_join_leave(template: str, member: discord.Member): {user_id} = user's ID """ template = template or "{user_mention}\n{user} ({user_id})" - return template.format(guild=member.guild, user=str(member), user_name=member.name, - user_mention=member.mention, user_id=member.id) + return template.format(guild=member.guild.name, user=str(member), user_name=member.name, + user_mention=member.mention, user_id=member.id) class CustomJoinLeaveMessages(db.DatabaseTable): diff --git a/requirements.txt b/requirements.txt index 5afc5e1d..651f502e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ asyncpg==0.26.0 -discord.py[speed,voice]==2.1.0 +discord.py[speed,voice]==2.2.3 aiotba~=0.0.3.post1 tbapi~=1.3.1b5 googlemaps~=4.4.2 From 458a95fee90e61ee7feb7bee4e6a251617cd994e Mon Sep 17 00:00:00 2001 From: Guinea Wheek Date: Tue, 16 May 2023 22:27:09 -0700 Subject: [PATCH 20/66] format() free replacement for format_join_leave --- dozer/Components/CustomJoinLeaveMessages.py | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/dozer/Components/CustomJoinLeaveMessages.py b/dozer/Components/CustomJoinLeaveMessages.py index f59f0499..3b3f8ba4 100644 --- a/dozer/Components/CustomJoinLeaveMessages.py +++ b/dozer/Components/CustomJoinLeaveMessages.py @@ -31,8 +31,26 @@ def format_join_leave(template: str, member: discord.Member): {user_id} = user's ID """ template = template or "{user_mention}\n{user} ({user_id})" - return template.format(guild=member.guild.name, user=str(member), user_name=member.name, - user_mention=member.mention, user_id=member.id) + subst = [("{guild}", member.guild.name), + ("{user}", member), + ("{user_name}", member.name), + ("{user_mention}", member.mention), + ("{user_id}", member.id)] + + def helper(s: str, subst: list): + if not subst: + # base case: return self + return s + cur = subst[0] + # split the current string on cur[0]. + # for each split segment call the helper on the rest of the substitutions. + # then rejoin on the substitutions (this avoids the substituted values from matching substitution keywords) + + # we could make this not recursive but there's an O(1) number of possible recursions anyway + # recursion depth is limited to 5 since the subst list is limited + # breadth is limited by template size (indirectly limited by discord message size) + return str(cur[1]).join([helper(bit, subst[1:]) for bit in s.split(cur[0])]) + return helper(template, subst) class CustomJoinLeaveMessages(db.DatabaseTable): From 5a68dcac53323c792e3d75f8976cc22e3bce3864 Mon Sep 17 00:00:00 2001 From: Guinea Wheek Date: Mon, 22 May 2023 20:19:53 -0700 Subject: [PATCH 21/66] add the member role first --- dozer/cogs/roles.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dozer/cogs/roles.py b/dozer/cogs/roles.py index 5d481c2a..4695d3d8 100755 --- a/dozer/cogs/roles.py +++ b/dozer/cogs/roles.py @@ -12,6 +12,7 @@ from dozer.context import DozerContext from ._utils import * from .actionlogs import CustomJoinLeaveMessages +from .moderation import MemberRole from .. import db from ..db import * @@ -152,6 +153,17 @@ async def on_member_join(self, member: discord.Member): if len(restore) == 0: return # New member - nothing to restore + # try and prioritize the member role first for Speed + member_role_id = None + stg = await MemberRole.get_by(guild_id=member.guild.id) + if stg: + member_role_id = stg[0].member_role + member_role = member.guild.get_role(member_role_id) + if member_role is not None and any(s.role_id == member_role_id for s in restore) and member_role.position <= top_restorable: + await member.add_roles(member_role) + else: + member_role_id = None + valid, cant_give, missing = set(), set(), set() for missing_role in restore: role = member.guild.get_role(missing_role.role_id) @@ -159,7 +171,7 @@ async def on_member_join(self, member: discord.Member): missing.add(missing_role.role_name) elif role.position > top_restorable: cant_give.add(role.name) - else: + elif role.id != member_role_id: valid.add(role) for entry in restore: # Not missing anymore - remove the record to free up the primary key From cfe3b02025c05b07c9b265cf7e72011809f94d7c Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:31:23 -0500 Subject: [PATCH 22/66] moved firstqa.py to correct folder Previous PR that I added this, I accidentally added it to the top-level folder rather than cogs. Also, corrected FTC->FRC on one of the comments for frc qa --- dozer/cogs/firstqa.py | 129 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 dozer/cogs/firstqa.py diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py new file mode 100644 index 00000000..c0154fea --- /dev/null +++ b/dozer/cogs/firstqa.py @@ -0,0 +1,129 @@ +"""Provides commands that pull information from First Q&A Form.""" +import discord +import aiohttp + +from bs4 import BeautifulSoup +from discord.ext import commands + +from context import DozerContext +from ._utils import * +from discord import app_commands + + +class QA(commands.Cog): + """QA commands""" + + def __init__(self, bot) -> None: + self.ses = aiohttp.ClientSession() + super().__init__() + self.bot = bot + + @commands.hybrid_command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) + @bot_has_permissions(embed_links = True) + @app_commands.describe(question = "The number of the question you want to look up") + async def ftcqa(self, ctx: DozerContext, question: int): + """ + Shows Answers from the FTC Q&A + """ + async with self.ses.get('https://ftc-qa.firstinspires.org/onepage.html') as response: + html_data = await response.text() + + answers = BeautifulSoup(html_data, 'html.parser').get_text() + + start = answers.find('Q' + str(question) + ' ') + a = "" + if start > 0: + + finish = answers.find('answered', start) + 24 + a = answers[start:finish] + + # remove newlines + a = a.replace("\n", " ") + + # remove multiple spaces + a = " ".join(a.split()) + + embed = discord.Embed( + title = a[:a.find(" Q: ")], + url = "https://ftc-qa.firstinspires.org/qa/" + str(question), + color = discord.Color.blue()) + + embed.add_field(name = "Question", + value = a[a.find(" Q: ") + 1:a.find(" A: ")], + inline = False) + embed.add_field(name = "Answer", + value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline = False) + + embed.set_footer( + text = a[a.find(" ( Asked by ") + 1:]) + + await ctx.send(embed = embed, ephemeral = True) + + else: + a = "That question was not answered or does not exist." + + # add url + a += "\nhttps://ftc-qa.firstinspires.org/qa/" + question + await ctx.send(a, ephemeral = True) + + ftcqa.example_usage = """ + `{prefix}ftcqa 19` - show information on FTC Q&A #19 + """ + + @commands.hybrid_command(name="frcqa", aliases=["frcqaforum"], pass_context=True) + @bot_has_permissions(embed_links = True) + @app_commands.describe(question = "The number of the question you want to look up") + async def frcqa(self, ctx: DozerContext, question: int): + """ + Shows Answers from the FRC Q&A + """ + async with self.ses.get('https://frc-qa.firstinspires.org/onepage.html') as response: + html_data = await response.text() + + answers = BeautifulSoup(html_data, 'html.parser').get_text() + + start = answers.find('Q' + str(question) + ' ') + a = "" + if start > 0: + + finish = answers.find('answered', start) + 24 + a = answers[start:finish] + + # remove newlines + a = a.replace("\n", " ") + + # remove multiple spaces + a = " ".join(a.split()) + + embed = discord.Embed( + title = a[:a.find(" Q: ")], + url = "https://frc-qa.firstinspires.org/qa/" + str(question), + color = discord.Color.blue()) + + embed.add_field(name = "Question", + value = a[a.find(" Q: ") + 1:a.find(" A: ")], + inline = False) + embed.add_field(name = "Answer", + value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline = False) + + embed.set_footer( + text = a[a.find(" ( Asked by ") + 1:]) + + await ctx.send(embed = embed, ephemeral = True) + + else: + a = "That question was not answered or does not exist." + + # add url + a += "\nhttps://frc-qa.firstinspires.org/qa/" + question + await ctx.send(a, ephemeral = True) + + frcqa.example_usage = """ + `{prefix}frcqa 19` - show information on FRC Q&A #19 + """ + +async def setup(bot): + """Adds the QA cog to the bot.""" + await bot.add_cog(QA(bot)) From 7ee1d574caab4a3807431aa2b6f1eac1bcd39895 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:36:45 -0500 Subject: [PATCH 23/66] Update firstqa.py idk why it didn't complain on the original pr that passed --- dozer/cogs/firstqa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index c0154fea..3cf7c08d 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -2,11 +2,11 @@ import discord import aiohttp +from ._utils import * from bs4 import BeautifulSoup -from discord.ext import commands -from context import DozerContext -from ._utils import * +from discord.ext import commands +from dozer.context import DozerContext from discord import app_commands From 70bfdb35f40d58597b240c6d97bf6dadd069c8c9 Mon Sep 17 00:00:00 2001 From: Travis Weir Date: Thu, 8 Jun 2023 23:48:30 -0500 Subject: [PATCH 24/66] [DRAFT] discriminator removal --- dozer/cogs/actionlogs.py | 2 +- dozer/cogs/info.py | 1 - dozer/cogs/maintenance.py | 2 +- dozer/cogs/modmail.py | 6 +++--- dozer/cogs/music.py | 8 ++++---- requirements.txt | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/dozer/cogs/actionlogs.py b/dozer/cogs/actionlogs.py index 4951e6e4..326039fa 100644 --- a/dozer/cogs/actionlogs.py +++ b/dozer/cogs/actionlogs.py @@ -300,7 +300,7 @@ async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): embed = discord.Embed(title="Message Edited", description=f"[MESSAGE]({link}) From {mention}\nEdited In: {mchannel.mention}", color=0xFFC400) - embed.set_author(name=f"{author['username']}#{author['discriminator']}", icon_url=avatar_link) + embed.set_author(name=f"{author['username']}{'#' + author['discriminator'] if author['discriminator'] != '0' else ''}", icon_url=avatar_link) embed.add_field(name="Original", value="N/A", inline=False) if content: embed.add_field(name="Edited", value=content[0:1023], inline=False) diff --git a/dozer/cogs/info.py b/dozer/cogs/info.py index 2af84e4f..a6c5a3b6 100755 --- a/dozer/cogs/info.py +++ b/dozer/cogs/info.py @@ -29,7 +29,6 @@ async def member(self, ctx: DozerContext, *, member: discord.Member = None): **This command works without mentions.** Remove the '@' before your mention so you don't ping the person unnecessarily. You can pick a member by: - Username (`cooldude`) - - Username and discriminator (`cooldude#1234`) - ID (`326749693969301506`) - Nickname - must be exact and is case-sensitive (`"Mr. Cool Dude III | Team 1234"`) - Mention (not recommended) (`@Mr Cool Dude III | Team 1234`) diff --git a/dozer/cogs/maintenance.py b/dozer/cogs/maintenance.py index dcd2b592..da61fe49 100755 --- a/dozer/cogs/maintenance.py +++ b/dozer/cogs/maintenance.py @@ -24,7 +24,7 @@ def cog_check(self, ctx: DozerContext): # All of this cog is only available to async def shutdown(self, ctx: DozerContext): """Force-stops the bot.""" await ctx.send('Shutting down') - logger.info(f'Shutting down at request of {ctx.author.name}#{ctx.author.discriminator} ' + logger.info(f'Shutting down at request of {ctx.author.name}{"#" + ctx.author.discriminator if ctx.author.discriminator != "0" else ""} ' f'(in {ctx.guild.name}, #{ctx.channel.name})') await self.bot.shutdown() diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index 82ddf3dd..1827a026 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -49,12 +49,12 @@ async def on_submit(self, interaction: discord.Interaction): # pylint: disable= timestamp=datetime.datetime.utcnow(), ) new_ticket_embed.set_footer( - text=f"{interaction.user.name}#{interaction.user.discriminator} | {interaction.user.id}", + text=f"{interaction.user.name}{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} | {interaction.user.id}", icon_url=interaction.user.avatar.url if interaction.user.avatar is not None else None, ) target_record = await ModmailConfig.get_by(guild_id=interaction.guild_id) mod_channel = interaction.client.get_channel(target_record[0].target_channel) - user_string = f"{interaction.user.name}#{interaction.user.discriminator} ({interaction.user.id})" + user_string = f"{interaction.user.name}{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} ({interaction.user.id})" if len(user_string) > 100: user_string = user_string[:96] + "..." mod_message = await mod_channel.send(user_string) @@ -105,7 +105,7 @@ async def send_modmail_embeds(self, source_channel, message_content, author, rec if len(to_send) > 3071: embed.add_field(name="Message (continued)", value=to_send[3072:4000]) embed.set_footer( - text=f"{author.name}#{author.discriminator} | {author.id} | {guild.name}", + text=f"{author.name}{'#' + author.discriminator if author.discriminator != '0' else ''} | {author.id} | {guild.name}", icon_url=author.avatar.url if author.avatar is not None else None, ) files = [] diff --git a/dozer/cogs/music.py b/dozer/cogs/music.py index f5ac1e1f..4465e26e 100644 --- a/dozer/cogs/music.py +++ b/dozer/cogs/music.py @@ -1,5 +1,5 @@ """Music commands, currently disabled""" -import lavaplayer +import lavaplay from discord.ext import commands from loguru import logger @@ -15,7 +15,7 @@ def __init__(self, bot): return llconfig = self.bot.config['lavalink'] - self.lavalink = lavaplayer.Lavalink( + self.lavalink = lavaplay.Lavalink( host=llconfig['host'], port=llconfig['port'], password=llconfig['password'], @@ -46,10 +46,10 @@ async def play(self, ctx: commands.Context, *, query: str): if not tracks: return await ctx.send("No results found.") - elif isinstance(tracks, lavaplayer.TrackLoadFailed): + elif isinstance(tracks, lavaplay.TrackLoadFailed): await ctx.send("Track load failed. Try again.\n```" + tracks.message + "```") # Playlist - elif isinstance(tracks, lavaplayer.PlayList): + elif isinstance(tracks, lavaplay.PlayList): msg = await ctx.send("Playlist found, Adding to queue, Please wait...") await self.lavalink.add_to_queue(ctx.guild.id, tracks.tracks, ctx.author.id) await msg.edit(content=f"Added to queue, tracks: {len(tracks.tracks)}, name: {tracks.name}") diff --git a/requirements.txt b/requirements.txt index 651f502e..a68055d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ asyncpg==0.26.0 -discord.py[speed,voice]==2.2.3 +git+https://github.com/Rapptz/discord.py aiotba~=0.0.3.post1 tbapi~=1.3.1b5 googlemaps~=4.4.2 @@ -12,5 +12,5 @@ sentry-sdk rstcloth~=0.3.1 humanize~=3.8.0 pre-commit~=2.20.0 -lavaplayer +lavaplay.py loguru~=0.6.0 \ No newline at end of file From 180329cbc8835c674fc07213f9a6e5d12df3e45e Mon Sep 17 00:00:00 2001 From: Travis Weir Date: Thu, 8 Jun 2023 23:50:53 -0500 Subject: [PATCH 25/66] [DRAFT] discriminator removal custom logs --- dozer/Components/CustomJoinLeaveMessages.py | 5 ++--- dozer/cogs/actionlogs.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dozer/Components/CustomJoinLeaveMessages.py b/dozer/Components/CustomJoinLeaveMessages.py index f59f0499..99b1075c 100644 --- a/dozer/Components/CustomJoinLeaveMessages.py +++ b/dozer/Components/CustomJoinLeaveMessages.py @@ -25,13 +25,12 @@ async def send_log(member): def format_join_leave(template: str, member: discord.Member): """Formats join leave message templates {guild} = guild name - {user} = user's name plus discriminator ex. SnowPlow#5196 - {user_name} = user's name without discriminator + {user} = user's name {user_mention} = user's mention {user_id} = user's ID """ template = template or "{user_mention}\n{user} ({user_id})" - return template.format(guild=member.guild.name, user=str(member), user_name=member.name, + return template.format(guild=member.guild.name, user=str(member), user_mention=member.mention, user_id=member.id) diff --git a/dozer/cogs/actionlogs.py b/dozer/cogs/actionlogs.py index 326039fa..13867cb2 100644 --- a/dozer/cogs/actionlogs.py +++ b/dozer/cogs/actionlogs.py @@ -547,8 +547,7 @@ async def help(self, e.set_footer(text='Triggered by ' + escape_markdown(ctx.author.display_name)) e.description = """ `{guild}` = guild name - `{user}` = user's name plus discriminator ex. SnowPlow#5196 - `{user_name}` = user's name without discriminator + `{user}` = user's name `{user_mention}` = user's mention `{user_id}` = user's ID """ From 4f054a934e0aa2a1d0f07cb2544433356a58f2ac Mon Sep 17 00:00:00 2001 From: Travis Weir Date: Thu, 8 Jun 2023 23:58:58 -0500 Subject: [PATCH 26/66] Delete music --- docker-compose.yml | 6 -- dozer/cogs/modmail.py | 8 ++- dozer/cogs/music.py | 134 ------------------------------------------ requirements.txt | 1 - 4 files changed, 6 insertions(+), 143 deletions(-) delete mode 100644 dozer/cogs/music.py diff --git a/docker-compose.yml b/docker-compose.yml index c021a0de..ff859e3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,12 +9,6 @@ services: - "5432:5432" volumes: - database:/var/lib/postgresql/data - lavalink: - image: fredboat/lavalink:latest - environment: - LAVALINK_SERVER_PASSWORD: youshallnotpass - SERVER_PORT: 2333 - SERVER_HOST: 0.0.0.0 dozer: depends_on: - postgres diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index 1827a026..290e0ec1 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -49,12 +49,16 @@ async def on_submit(self, interaction: discord.Interaction): # pylint: disable= timestamp=datetime.datetime.utcnow(), ) new_ticket_embed.set_footer( - text=f"{interaction.user.name}{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} | {interaction.user.id}", + text=f"{interaction.user.name}" + f"{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} " + f"| {interaction.user.id}", icon_url=interaction.user.avatar.url if interaction.user.avatar is not None else None, ) target_record = await ModmailConfig.get_by(guild_id=interaction.guild_id) mod_channel = interaction.client.get_channel(target_record[0].target_channel) - user_string = f"{interaction.user.name}{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} ({interaction.user.id})" + user_string = f"{interaction.user.name}" \ + f"{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} " \ + f"({interaction.user.id})" if len(user_string) > 100: user_string = user_string[:96] + "..." mod_message = await mod_channel.send(user_string) diff --git a/dozer/cogs/music.py b/dozer/cogs/music.py deleted file mode 100644 index 4465e26e..00000000 --- a/dozer/cogs/music.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Music commands, currently disabled""" -import lavaplay -from discord.ext import commands -from loguru import logger - -from dozer.cogs._utils import command - - -class Music(commands.Cog): - """Music commands cog""" - - def __init__(self, bot): - self.bot = bot - if not self.bot.config['lavalink']['enabled']: - return - - llconfig = self.bot.config['lavalink'] - self.lavalink = lavaplay.Lavalink( - host=llconfig['host'], - port=llconfig['port'], - password=llconfig['password'], - ) - self.lavalink.set_user_id(bot.user.id) - self.lavalink.connect() - - @command() - async def disconnect(self, ctx: commands.Context): - """Disconnects from voice channel""" - await ctx.guild.change_voice_state(channel=None) - await self.lavalink.wait_for_remove_connection(ctx.guild.id) - await ctx.send("Left the voice channel.") - - async def join(self, ctx: commands.Context): - """Joins the voice channel""" - print(ctx.author.voice) - await ctx.guild.change_voice_state(channel=ctx.author.voice.channel, self_deaf=True, self_mute=False) - print("Awaiting connection") - await self.lavalink.wait_for_connection(ctx.guild.id) - print("Join successful") - - @command() - async def play(self, ctx: commands.Context, *, query: str): - """Plays a song""" - await self.join(ctx) - tracks = await self.lavalink.auto_search_tracks(query) - - if not tracks: - return await ctx.send("No results found.") - elif isinstance(tracks, lavaplay.TrackLoadFailed): - await ctx.send("Track load failed. Try again.\n```" + tracks.message + "```") - # Playlist - elif isinstance(tracks, lavaplay.PlayList): - msg = await ctx.send("Playlist found, Adding to queue, Please wait...") - await self.lavalink.add_to_queue(ctx.guild.id, tracks.tracks, ctx.author.id) - await msg.edit(content=f"Added to queue, tracks: {len(tracks.tracks)}, name: {tracks.name}") - return - await self.lavalink.wait_for_connection(ctx.guild.id) - await self.lavalink.play(ctx.guild.id, tracks[0], ctx.author.id) - await ctx.send(f"Now playing: {tracks[0].title}") - - @command() - async def pause(self, ctx: commands.Context): - """Pauses the current song""" - await self.lavalink.pause(ctx.guild.id, True) - await ctx.send("Paused the track.") - - @command() - async def resume(self, ctx: commands.Context): - """Resume the current song""" - await self.lavalink.pause(ctx.guild.id, False) - await ctx.send("Resumed the track.") - - @command() - async def stop(self, ctx: commands.Context): - """Stop the current song""" - await self.lavalink.stop(ctx.guild.id) - await ctx.send("Stopped the track.") - - @command() - async def skip(self, ctx: commands.Context): - """Skip the current song""" - await self.lavalink.skip(ctx.guild.id) - await ctx.send("Skipped the track.") - - @command() - async def queue(self, ctx: commands.Context): - """Get queue info""" - queue = await self.lavalink.queue(ctx.guild.id) - if not queue: - return await ctx.send("No tracks in queue.") - tracks = [f"**{i + 1}.** {t.title}" for (i, t) in enumerate(queue)] - await ctx.send("\n".join(tracks)) - - @command() - async def volume(self, ctx: commands.Context, volume: int): - """Set the volume""" - await self.lavalink.volume(ctx.guild.id, volume) - await ctx.send(f"Set the volume to {volume}%.") - - @command() - async def seek(self, ctx: commands.Context, seconds: int): - """Seek to a timestamp in the current song""" - await self.lavalink.seek(ctx.guild.id, seconds) - await ctx.send(f"Seeked to {seconds} seconds.") - - @command() - async def shuffle(self, ctx: commands.Context): - """Shuffle the queue""" - await self.lavalink.shuffle(ctx.guild.id) - await ctx.send("Shuffled the queue.") - - @command() - async def remove(self, ctx: commands.Context, index: int): - """Removes a song from the queue""" - await self.lavalink.remove(ctx.guild.id, index) - await ctx.send(f"Removed track {index}.") - - @command() - async def clear(self, ctx: commands.Context): - """Clears the queue""" - await self.lavalink.stop(ctx.guild.id) - await ctx.send("Cleared the queue.") - - @command() - async def repeat(self, ctx: commands.Context, status: bool): - """Repeats the queue""" - await self.lavalink.repeat(ctx.guild.id, status) - await ctx.send("Repeated the queue.") - - -async def setup(bot: commands.Bot): - """Adds the cog to the bot""" - # await bot.add_cog(Music(bot)) - logger.info("Music cog is temporarily disabled due to code bugs.") diff --git a/requirements.txt b/requirements.txt index a68055d7..399e8439 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,4 @@ sentry-sdk rstcloth~=0.3.1 humanize~=3.8.0 pre-commit~=2.20.0 -lavaplay.py loguru~=0.6.0 \ No newline at end of file From a67fa382f6706f97b9c4b99cdba9d3d730452534 Mon Sep 17 00:00:00 2001 From: Travis Weir Date: Fri, 9 Jun 2023 00:00:48 -0500 Subject: [PATCH 27/66] Delete music properly --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ff859e3f..aae79c58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,6 @@ services: dozer: depends_on: - postgres - - lavalink build: . volumes: - ".:/app" From 23a8c01902b52c4d61837164888900e82adeee2c Mon Sep 17 00:00:00 2001 From: Travis Weir Date: Mon, 12 Jun 2023 14:03:22 -0500 Subject: [PATCH 28/66] Update for production version, all functions good --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 399e8439..eb01aff2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ asyncpg==0.26.0 -git+https://github.com/Rapptz/discord.py +discord.py[speed,voice]==2.3.0 aiotba~=0.0.3.post1 tbapi~=1.3.1b5 googlemaps~=4.4.2 From f9a2d0cfe4ab94c2f6737f1eeb117dde67dd32c0 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:38:02 -0500 Subject: [PATCH 29/66] removed ephemeral :( --- dozer/cogs/toa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dozer/cogs/toa.py b/dozer/cogs/toa.py index 61b82e0c..5c3d3322 100755 --- a/dozer/cogs/toa.py +++ b/dozer/cogs/toa.py @@ -85,7 +85,7 @@ async def team(self, ctx: DozerContext, team_num: int): value=', '.join((team_data['city'], team_data['state_prov'], team_data['country']))) e.add_field(name='Website', value=team_data['website'] or 'n/a') e.add_field(name='Team Info Page', value=f'https://theorangealliance.org/teams/{team_data["team_key"]}') - await ctx.send('', embed=e, ephemeral=True) + await ctx.send('', embed=e) team.example_usage = """ `{prefix}toa team 12670` - show information on team 12670, Eclipse From 82b8def79a72b6c990513fa7e469de4e878fddd3 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:56:57 -0500 Subject: [PATCH 30/66] used chatgpt to create helper function to scrape data, and removed ephemeral :( --- dozer/cogs/firstqa.py | 148 ++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 86 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 3cf7c08d..c290a18f 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -1,13 +1,60 @@ """Provides commands that pull information from First Q&A Form.""" import discord +from discord.ext import commands +from discord.context import DozerContext +from discord import app_commands +from typing import Union + import aiohttp from ._utils import * from bs4 import BeautifulSoup -from discord.ext import commands -from dozer.context import DozerContext -from discord import app_commands + +async def data(ctx: DozerContext, level: str, question: int) -> Union[str, None]: + """Returns QA Forum info for specified FTC/FRC""" + if level.lower() == "ftc": + async with ctx.cog.ses.get('https://ftc-qa.firstinspires.org/onepage.html') as response: + html_data = await response.text() + forum_url = "https://ftc-qa.firstinspires.org/qa/" + elif level.lower() == "frc": + async with ctx.cog.ses.get('https://frc-qa.firstinspires.org/onepage.html') as response: + html_data = await response.text() + forum_url = "https://frc-qa.firstinspires.org/qa/" + else: + return None + + answers = BeautifulSoup(html_data, 'html.parser').get_text() + + start = answers.find(f'Q{question} ') + a = "" + if start > 0: + finish = answers.find('answered', start) + 24 + a = answers[start:finish] + # remove newlines + a = a.replace("\n", " ") + # remove multiple spaces + a = " ".join(a.split()) + embed = discord.Embed( + title=a[:a.find(" Q: ")], + url=forum_url + str(question), + color=discord.Color.blue() + ) + embed.add_field( + name="Question", + value=a[a.find(" Q: ") + 1:a.find(" A: ")], + inline=False + ) + embed.add_field( + name="Answer", + value=a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline=False + ) + embed.set_footer(text=a[a.find(" ( Asked by ") + 1:]) + return embed + + else: + return f"That question was not answered or does not exist.\n{forum_url + str(question)}" class QA(commands.Cog): @@ -19,111 +66,40 @@ def __init__(self, bot) -> None: self.bot = bot @commands.hybrid_command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) - @bot_has_permissions(embed_links = True) - @app_commands.describe(question = "The number of the question you want to look up") + @bot_has_permissions(embed_links=True) + @app_commands.describe(question="The number of the question you want to look up") async def ftcqa(self, ctx: DozerContext, question: int): """ Shows Answers from the FTC Q&A """ - async with self.ses.get('https://ftc-qa.firstinspires.org/onepage.html') as response: - html_data = await response.text() - - answers = BeautifulSoup(html_data, 'html.parser').get_text() - - start = answers.find('Q' + str(question) + ' ') - a = "" - if start > 0: - - finish = answers.find('answered', start) + 24 - a = answers[start:finish] - - # remove newlines - a = a.replace("\n", " ") - - # remove multiple spaces - a = " ".join(a.split()) - - embed = discord.Embed( - title = a[:a.find(" Q: ")], - url = "https://ftc-qa.firstinspires.org/qa/" + str(question), - color = discord.Color.blue()) - - embed.add_field(name = "Question", - value = a[a.find(" Q: ") + 1:a.find(" A: ")], - inline = False) - embed.add_field(name = "Answer", - value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], - inline = False) - - embed.set_footer( - text = a[a.find(" ( Asked by ") + 1:]) - - await ctx.send(embed = embed, ephemeral = True) - + result = await data(ctx, "ftc", question) + if isinstance(result, discord.Embed): + await ctx.send(embed=result) else: - a = "That question was not answered or does not exist." - - # add url - a += "\nhttps://ftc-qa.firstinspires.org/qa/" + question - await ctx.send(a, ephemeral = True) + await ctx.send(result) ftcqa.example_usage = """ `{prefix}ftcqa 19` - show information on FTC Q&A #19 """ - @commands.hybrid_command(name="frcqa", aliases=["frcqaforum"], pass_context=True) + @commands.hybrid_command(name = "frcqa", aliases = ["frcqaforum"], pass_context = True) @bot_has_permissions(embed_links = True) @app_commands.describe(question = "The number of the question you want to look up") async def frcqa(self, ctx: DozerContext, question: int): """ Shows Answers from the FRC Q&A """ - async with self.ses.get('https://frc-qa.firstinspires.org/onepage.html') as response: - html_data = await response.text() - - answers = BeautifulSoup(html_data, 'html.parser').get_text() - - start = answers.find('Q' + str(question) + ' ') - a = "" - if start > 0: - - finish = answers.find('answered', start) + 24 - a = answers[start:finish] - - # remove newlines - a = a.replace("\n", " ") - - # remove multiple spaces - a = " ".join(a.split()) - - embed = discord.Embed( - title = a[:a.find(" Q: ")], - url = "https://frc-qa.firstinspires.org/qa/" + str(question), - color = discord.Color.blue()) - - embed.add_field(name = "Question", - value = a[a.find(" Q: ") + 1:a.find(" A: ")], - inline = False) - embed.add_field(name = "Answer", - value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], - inline = False) - - embed.set_footer( - text = a[a.find(" ( Asked by ") + 1:]) - - await ctx.send(embed = embed, ephemeral = True) - + result = await data(ctx, "frc", question) + if isinstance(result, discord.Embed): + await ctx.send(embed=result) else: - a = "That question was not answered or does not exist." - - # add url - a += "\nhttps://frc-qa.firstinspires.org/qa/" + question - await ctx.send(a, ephemeral = True) + await ctx.send(result) frcqa.example_usage = """ `{prefix}frcqa 19` - show information on FRC Q&A #19 """ + async def setup(bot): """Adds the QA cog to the bot.""" await bot.add_cog(QA(bot)) From 647270a84423e70118d53fffcda359ec52039c7b Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 12 Jun 2023 23:03:54 -0500 Subject: [PATCH 31/66] fixing weird stuff that pylint doesn't like (and one discord->dozer import error) --- dozer/cogs/firstqa.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index c290a18f..724ff8f3 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -1,15 +1,17 @@ """Provides commands that pull information from First Q&A Form.""" +from typing import Union + import discord from discord.ext import commands -from discord.context import DozerContext from discord import app_commands -from typing import Union import aiohttp -from ._utils import * from bs4 import BeautifulSoup +from ._utils import * +from dozer.context import DozerContext + async def data(ctx: DozerContext, level: str, question: int) -> Union[str, None]: """Returns QA Forum info for specified FTC/FRC""" From a8d9095cff9eca864754f7a9374b838e62caa14c Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 12 Jun 2023 23:15:49 -0500 Subject: [PATCH 32/66] removed ephemeral :( --- dozer/cogs/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/info.py b/dozer/cogs/info.py index 7ade397b..ddd08c0b 100755 --- a/dozer/cogs/info.py +++ b/dozer/cogs/info.py @@ -87,7 +87,7 @@ async def member(self, ctx: DozerContext, *, member: discord.Member = None): embed.add_field(name='Roles', value=', '.join(role.mention for role in roles) or 'None', inline=False) footers.append(f"Color: {str(member.color).upper()}") embed.set_footer(text="; ".join(footers)) - await ctx.send(embed=embed, ephemeral=True) + await ctx.send(embed=embed) member.example_usage = """ `{prefix}member`: show your member info @@ -184,7 +184,7 @@ async def guild(self, ctx: DozerContext): f'{ctx.guild.premium_subscription_count} booster(s)\n' f'{ctx.guild.filesize_limit // 1024**2}MiB files, ' f'{ctx.guild.bitrate_limit / 1000:0.1f}kbps voice') - await ctx.send(embed = e, ephemeral = True) + await ctx.send(embed = e) guild.example_usage = """ `{prefix}guild` - get information about this guild @@ -206,7 +206,7 @@ async def stats(self, ctx: DozerContext): "Process uptime": str(datetime.timedelta(seconds = round(time.time() - startup_time))) }.items())) embed = discord.Embed(title = f"Stats for {info.name}", description = f"Bot owner: {info.owner.mention}```{frame}```", color = blurple) - await ctx.send(embed=embed, ephemeral = True) + await ctx.send(embed=embed) stats.example_usage = """ `{prefix}stats` - get current bot/host stats From e06f047e40b5cef626bb491a6ff7fbfd190fb46a Mon Sep 17 00:00:00 2001 From: Abigail Date: Wed, 19 Jul 2023 16:43:23 -0400 Subject: [PATCH 33/66] Fixes error from toa team when team does not exist If the team does not exist, TOA now returns a 404 status code, this checks for that condition so a proper error message can be displayed to the user instead of a python exception. --- dozer/cogs/toa.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dozer/cogs/toa.py b/dozer/cogs/toa.py index 4e0cab04..d6f15b3b 100755 --- a/dozer/cogs/toa.py +++ b/dozer/cogs/toa.py @@ -49,6 +49,8 @@ async def req(self, endpoint): try: async with async_timeout.timeout(5) as _, self.http.get(urljoin(self.base, endpoint), headers=self.headers) as response: + if response.status == 404: + return "[]" return await response.text() except aiohttp.ClientError: tries += 1 From 0de51fa060cfa49da767ac0d354ac871b656339b Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:19:36 -0500 Subject: [PATCH 34/66] Fixed import order --- dozer/cogs/firstqa.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 724ff8f3..db0bbc01 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -1,18 +1,17 @@ """Provides commands that pull information from First Q&A Form.""" from typing import Union +import aiohttp +from bs4 import BeautifulSoup import discord from discord.ext import commands from discord import app_commands -import aiohttp - -from bs4 import BeautifulSoup - from ._utils import * from dozer.context import DozerContext + async def data(ctx: DozerContext, level: str, question: int) -> Union[str, None]: """Returns QA Forum info for specified FTC/FRC""" if level.lower() == "ftc": From 27cf5ed02fd863374a04886b065a2c54d237d0cb Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:23:39 -0500 Subject: [PATCH 35/66] pylint can't find beautifulsoup for some reason (it is the right line to my knowledge), and hopefully pylint won't be mad this time about dozer.context after -_utils --- dozer/cogs/firstqa.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index db0bbc01..10d17650 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -7,9 +7,8 @@ from discord.ext import commands from discord import app_commands -from ._utils import * from dozer.context import DozerContext - +from ._utils import * async def data(ctx: DozerContext, level: str, question: int) -> Union[str, None]: From 22f72dca27ddbd1b7429c7cec871c1a4262afa7d Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:26:22 -0500 Subject: [PATCH 36/66] added bs4 to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 651f502e..cc30cf55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ rstcloth~=0.3.1 humanize~=3.8.0 pre-commit~=2.20.0 lavaplayer -loguru~=0.6.0 \ No newline at end of file +loguru~=0.6.0 +bs4==4.12.2 \ No newline at end of file From c0baba841742348a87567281cfd468ff82eda13f Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:28:19 -0500 Subject: [PATCH 37/66] my branch was out of date? --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index cc30cf55..7fb2a777 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ asyncpg==0.26.0 -discord.py[speed,voice]==2.2.3 +discord.py[speed,voice]==2.3.0 aiotba~=0.0.3.post1 tbapi~=1.3.1b5 googlemaps~=4.4.2 @@ -12,6 +12,5 @@ sentry-sdk rstcloth~=0.3.1 humanize~=3.8.0 pre-commit~=2.20.0 -lavaplayer loguru~=0.6.0 bs4==4.12.2 \ No newline at end of file From a71cd1c6efe85a88d48de5118c08d1549d19df76 Mon Sep 17 00:00:00 2001 From: travis weir Date: Tue, 31 Oct 2023 23:16:37 -0500 Subject: [PATCH 38/66] Fix inconsistent commands and formatting --- dozer/cogs/info.py | 41 ++++++++++++++++-------------- dozer/cogs/toa.py | 2 +- ftcqa.py | 63 ++++++++++++++++++++++++---------------------- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/dozer/cogs/info.py b/dozer/cogs/info.py index e595b504..c6e3d251 100755 --- a/dozer/cogs/info.py +++ b/dozer/cogs/info.py @@ -8,6 +8,7 @@ import discord import humanize +from discord.ext import commands from discord.ext.commands import cooldown, BucketType, guild_only from discord.utils import escape_markdown @@ -32,7 +33,7 @@ class Info(Cog): def __init__(self, bot): super().__init__(bot) self.bot = bot - + @command(aliases=['user', 'memberinfo', 'userinfo']) @guild_only() @bot_has_permissions(embed_links=True) @@ -54,7 +55,8 @@ async def member(self, ctx: DozerContext, *, member: discord.Member = None): levels_settings = await GuildXPSettings.get_by(guild_id=ctx.guild.id) levels_enabled = levels_settings[0].enabled if len(levels_settings) else False - embed = discord.Embed(title=escape_markdown(member.display_name), description=f'{member!s} ({member.id})', color=member.color) + embed = discord.Embed(title=escape_markdown(member.display_name), description=f'{member!s} ({member.id})', + color=member.color) embed.set_thumbnail(url=member.display_avatar) embed.add_field(name='Bot Created' if member.bot else 'Account Created', value=f"", inline=True) @@ -143,7 +145,8 @@ def pluralize(values: typing.List[str]) -> str: @cooldown(1, 10, BucketType.channel) async def role(self, ctx: DozerContext, role: discord.Role): """Retrieve info about a role in this guild""" - embed = discord.Embed(title=f"Info for role: {role.name}", description=f"{role.mention} ({role.id})", color=role.color) + embed = discord.Embed(title=f"Info for role: {role.name}", description=f"{role.mention} ({role.id})", + color=role.color) embed.add_field(name="Created on", value=f"") embed.add_field(name="Position", value=role.position) embed.add_field(name="Color", value=str(role.color).upper()) @@ -165,31 +168,31 @@ async def rolemembers(self, ctx: DozerContext, role: discord.Role): @guild_only() @cooldown(1, 10, BucketType.channel) - @commands.hybrid_command(name = "server", aliases = ['guild', 'guildinfo', 'serverinfo']) + @command(name="server", aliases=['guild', 'guildinfo', 'serverinfo']) async def guild(self, ctx: DozerContext): """Retrieve information about this guild.""" guild = ctx.guild static_emoji = sum(not e.animated for e in ctx.guild.emojis) animated_emoji = sum(e.animated for e in ctx.guild.emojis) - e = discord.Embed(color = blurple) - e.set_thumbnail(url = guild.icon.url if guild.icon else None) + e = discord.Embed(color=blurple) + e.set_thumbnail(url=guild.icon.url if guild.icon else None) e.title = guild.name e.description = f"{guild.member_count} members, {len(guild.channels)} channels, {len(guild.roles) - 1} roles" - e.add_field(name = 'ID', value = guild.id) - e.add_field(name = 'Created on', value = discord.utils.format_dt(guild.created_at)) - e.add_field(name = 'Owner', value = guild.owner.mention) - e.add_field(name = 'Emoji', value = f"{static_emoji} static, {animated_emoji} animated") - e.add_field(name = 'Nitro Boost', value = f'Level {ctx.guild.premium_tier}, ' - f'{ctx.guild.premium_subscription_count} booster(s)\n' - f'{ctx.guild.filesize_limit // 1024**2}MiB files, ' - f'{ctx.guild.bitrate_limit / 1000:0.1f}kbps voice') - await ctx.send(embed = e) + e.add_field(name='ID', value=guild.id) + e.add_field(name='Created on', value=discord.utils.format_dt(guild.created_at)) + e.add_field(name='Owner', value=guild.owner.mention) + e.add_field(name='Emoji', value=f"{static_emoji} static, {animated_emoji} animated") + e.add_field(name='Nitro Boost', value=f'Level {ctx.guild.premium_tier}, ' + f'{ctx.guild.premium_subscription_count} booster(s)\n' + f'{ctx.guild.filesize_limit // 1024 ** 2}MiB files, ' + f'{ctx.guild.bitrate_limit / 1000:0.1f}kbps voice') + await ctx.send(embed=e) guild.example_usage = """ `{prefix}guild` - get information about this guild """ - @commands.hybrid_command() + @command() async def stats(self, ctx: DozerContext): """Get current running internal/host stats for the bot""" info = await ctx.bot.application_info() @@ -202,16 +205,16 @@ async def stats(self, ctx: DozerContext): "": "", f"{' Host stats ':=^48}": "", "Operating system:": os_name, - "Process uptime": str(datetime.timedelta(seconds = round(time.time() - startup_time))) + "Process uptime": str(datetime.timedelta(seconds=round(time.time() - startup_time))) }.items())) - embed = discord.Embed(title = f"Stats for {info.name}", description = f"Bot owner: {info.owner.mention}```{frame}```", color = blurple) + embed = discord.Embed(title=f"Stats for {info.name}", + description=f"Bot owner: {info.owner.mention}```{frame}```", color=blurple) await ctx.send(embed=embed) stats.example_usage = """ `{prefix}stats` - get current bot/host stats """ - async def setup(bot): """Adds the info cog to the bot""" diff --git a/dozer/cogs/toa.py b/dozer/cogs/toa.py index c058dac7..6d225731 100755 --- a/dozer/cogs/toa.py +++ b/dozer/cogs/toa.py @@ -66,7 +66,7 @@ def __init__(self, bot: commands.Bot): self.http_session = bot.add_aiohttp_ses(aiohttp.ClientSession()) self.parser = TOAParser(bot.config['toa']['key'], self.http_session, app_name=bot.config['toa']['app_name']) - @commands.hybrid_command(name="toateam", aliases=["toa", "ftcteam", "ftcteaminfo"]) + @command(name="toateam", aliases=["toa", "ftcteam", "ftcteaminfo"]) @bot_has_permissions(embed_links=True) @app_commands.describe(team_num="The team you want to see toa info about") async def team(self, ctx: DozerContext, team_num: int): diff --git a/ftcqa.py b/ftcqa.py index 94d9438c..94deda13 100644 --- a/ftcqa.py +++ b/ftcqa.py @@ -6,6 +6,8 @@ from discord.ext import commands from context import DozerContext + +from dozer.cogs._utils import bot_has_permissions, command from ._utils import * from discord import app_commands @@ -18,9 +20,9 @@ def __init__(self, bot) -> None: super().__init__() self.bot = bot - @commands.hybrid_command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) - @bot_has_permissions(embed_links = True) - @app_commands.describe(question = "The number of the question you want to look up") + @command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) + @bot_has_permissions(embed_links=True) + @app_commands.describe(question="The number of the question you want to look up") async def ftcqa(self, ctx: DozerContext, question: int): """ Shows Answers from the FTC Q&A @@ -44,36 +46,36 @@ async def ftcqa(self, ctx: DozerContext, question: int): a = " ".join(a.split()) embed = discord.Embed( - title = a[:a.find(" Q: ")], - url = "https://ftc-qa.firstinspires.org/qa/" + str(question), - color = discord.Color.blue()) + title=a[:a.find(" Q: ")], + url="https://ftc-qa.firstinspires.org/qa/" + str(question), + color=discord.Color.blue()) - embed.add_field(name = "Question", - value = a[a.find(" Q: ") + 1:a.find(" A: ")], - inline = False) - embed.add_field(name = "Answer", - value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], - inline = False) + embed.add_field(name="Question", + value=a[a.find(" Q: ") + 1:a.find(" A: ")], + inline=False) + embed.add_field(name="Answer", + value=a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline=False) embed.set_footer( - text = a[a.find(" ( Asked by ") + 1:]) + text=a[a.find(" ( Asked by ") + 1:]) - await ctx.send(embed = embed, ephemeral = True) + await ctx.send(embed=embed, ephemeral=True) else: a = "That question was not answered or does not exist." # add url a += "\nhttps://ftc-qa.firstinspires.org/qa/" + question - await ctx.send(a, ephemeral = True) + await ctx.send(a, ephemeral=True) ftcqa.example_usage = """ `{prefix}ftcqa 19` - show information on FTC Q&A #19 """ - @commands.hybrid_command(name="frcqa", aliases=["frcqaforum"], pass_context=True) - @bot_has_permissions(embed_links = True) - @app_commands.describe(question = "The number of the question you want to look up") + @command(name="frcqa", aliases=["frcqaforum"], pass_context=True) + @bot_has_permissions(embed_links=True) + @app_commands.describe(question="The number of the question you want to look up") async def frcqa(self, ctx: DozerContext, question: int): """ Shows Answers from the FTC Q&A @@ -97,33 +99,34 @@ async def frcqa(self, ctx: DozerContext, question: int): a = " ".join(a.split()) embed = discord.Embed( - title = a[:a.find(" Q: ")], - url = "https://frc-qa.firstinspires.org/qa/" + str(question), - color = discord.Color.blue()) + title=a[:a.find(" Q: ")], + url="https://frc-qa.firstinspires.org/qa/" + str(question), + color=discord.Color.blue()) - embed.add_field(name = "Question", - value = a[a.find(" Q: ") + 1:a.find(" A: ")], - inline = False) - embed.add_field(name = "Answer", - value = a[a.find(" A: ") + 1:a.find(" ( Asked by ")], - inline = False) + embed.add_field(name="Question", + value=a[a.find(" Q: ") + 1:a.find(" A: ")], + inline=False) + embed.add_field(name="Answer", + value=a[a.find(" A: ") + 1:a.find(" ( Asked by ")], + inline=False) embed.set_footer( - text = a[a.find(" ( Asked by ") + 1:]) + text=a[a.find(" ( Asked by ") + 1:]) - await ctx.send(embed = embed, ephemeral = True) + await ctx.send(embed=embed, ephemeral=True) else: a = "That question was not answered or does not exist." # add url a += "\nhttps://frc-qa.firstinspires.org/qa/" + question - await ctx.send(a, ephemeral = True) + await ctx.send(a, ephemeral=True) frcqa.example_usage = """ `{prefix}frcqa 19` - show information on FRC Q&A #19 """ + async def setup(bot): """Adds the QA cog to the bot.""" await bot.add_cog(QA(bot)) From 175e2bca569f6b5e9742e84a832ec186e072b669 Mon Sep 17 00:00:00 2001 From: travis weir Date: Tue, 31 Oct 2023 23:17:11 -0500 Subject: [PATCH 39/66] Debugging (how I figured out CloudFlare broke it) --- dozer/sources/RSSSources.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dozer/sources/RSSSources.py b/dozer/sources/RSSSources.py index a1f025df..2db2d51f 100644 --- a/dozer/sources/RSSSources.py +++ b/dozer/sources/RSSSources.py @@ -33,6 +33,8 @@ def __init__(self, aiohttp_session: aiohttp.ClientSession, bot): async def first_run(self): """Fetch the current posts in the feed and add them to the guids_seen set""" response = await self.fetch() + if isinstance(self, CDLatest): + print("response") self.parse(response, True) async def get_new_posts(self): From 36125d6aefe76799884865c3ae74edf532dba4f8 Mon Sep 17 00:00:00 2001 From: Travis Weir Date: Wed, 1 Nov 2023 13:47:22 -0500 Subject: [PATCH 40/66] JVN blog UA workaround breaks CD, JVN blog unused anyways --- dozer/cogs/news.py | 3 +-- dozer/sources/RSSSources.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/dozer/cogs/news.py b/dozer/cogs/news.py index e005eaa6..273fe330 100644 --- a/dozer/cogs/news.py +++ b/dozer/cogs/news.py @@ -124,9 +124,8 @@ async def startup(self): if self.http_source: await self.http_source.close() self.http_source = aiohttp.ClientSession( - headers={'Connection': 'keep-alive', 'User-Agent': 'Dozer RSS Feed Reader'}) + headers={'Connection': 'keep-alive'}) self.bot.add_aiohttp_ses(self.http_source) - # JVN's blog will 403 you if you use the default user agent, so replacing it with this will yield a parsable result. for source in self.enabled_sources: try: self.sources[source.short_name] = source(aiohttp_session=self.http_source, bot=self.bot) diff --git a/dozer/sources/RSSSources.py b/dozer/sources/RSSSources.py index 2db2d51f..a1f025df 100644 --- a/dozer/sources/RSSSources.py +++ b/dozer/sources/RSSSources.py @@ -33,8 +33,6 @@ def __init__(self, aiohttp_session: aiohttp.ClientSession, bot): async def first_run(self): """Fetch the current posts in the feed and add them to the guids_seen set""" response = await self.fetch() - if isinstance(self, CDLatest): - print("response") self.parse(response, True) async def get_new_posts(self): From ae126ff2d612d865783a161ea91e0909a0e7493d Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Wed, 1 Nov 2023 19:15:52 -0500 Subject: [PATCH 41/66] idk --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7335a1d9..0618fa55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,4 @@ rstcloth~=0.3.1 humanize~=3.8.0 pre-commit~=2.20.0 loguru~=0.6.0 -bs4==4.12.2 +bs4 From 6259f48b15951c8e53d6f228d4a8ca4e0e73ddca Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Tue, 21 Nov 2023 18:04:04 +0000 Subject: [PATCH 42/66] Add frcrule command --- dozer/cogs/firstqa.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 10d17650..ff1ec0aa 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -100,6 +100,23 @@ async def frcqa(self, ctx: DozerContext, question: int): """ + @commands.hybrid_command(name = "frcrule", pass_context = True) + @bot_has_permissions(embed_links = True) + @app_commands.describe(rule = "The rule number") + async def frcrule(self, ctx: DozerContext, rule: str): + letter_part = ''.join([char for char in rule if char.isalpha()]) + number_part = ''.join([char for char in rule if char.isdigit()]) + + if not letter_part or not number_part: + await ctx.send("Not a valid rule.") + else: + # Construct the URL + url = f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}" + await ctx.send(url) + frcrule.example_usage = """ + `{prefix}frcrule G301` - sends a link to rule G301 + """ + async def setup(bot): """Adds the QA cog to the bot.""" await bot.add_cog(QA(bot)) From 4aa19c16c537dd76d95ae5065dbb270683b70e2b Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Tue, 21 Nov 2023 18:08:58 +0000 Subject: [PATCH 43/66] make errors ephemeral --- dozer/cogs/firstqa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index ff1ec0aa..5513ae91 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -108,7 +108,7 @@ async def frcrule(self, ctx: DozerContext, rule: str): number_part = ''.join([char for char in rule if char.isdigit()]) if not letter_part or not number_part: - await ctx.send("Not a valid rule.") + await ctx.send("Not a valid rule.", ephemeral=True) else: # Construct the URL url = f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}" From ef911a3b09cd674860e424a5d3800e54c65f77e3 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Tue, 21 Nov 2023 19:33:44 +0000 Subject: [PATCH 44/66] send content + better error handling --- .gitpod.yml | 10 ++++++++++ dozer/cogs/firstqa.py | 23 ++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 .gitpod.yml diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000..b1c4d2ba --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,10 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) +# and commit this file to your remote git repository to share the goodness with others. + +# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart + +tasks: + - init: pip install -r requirements.txt + + diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 5513ae91..f73eec1c 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -1,5 +1,6 @@ """Provides commands that pull information from First Q&A Form.""" from typing import Union +import re import aiohttp from bs4 import BeautifulSoup @@ -106,15 +107,23 @@ async def frcqa(self, ctx: DozerContext, question: int): async def frcrule(self, ctx: DozerContext, rule: str): letter_part = ''.join([char for char in rule if char.isalpha()]) number_part = ''.join([char for char in rule if char.isdigit()]) - - if not letter_part or not number_part: - await ctx.send("Not a valid rule.", ephemeral=True) + + if not re.match(r'^[a-zA-Z]\d{3}$', rule): + await ctx.send("Invalid rule number") + else: - # Construct the URL - url = f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}" - await ctx.send(url) + async with ctx.cog.ses.get('https://firstfrc.blob.core.windows.net/frc2023/Manual/HTML/2023FRCGameManual.htm') as response: + html_data = await response.content.read() + ruleSoup = BeautifulSoup(html_data, 'html.parser') + + result = ruleSoup.find("a", attrs={"name": f"{letter_part.upper()}{number_part}"}) + if result is not None: + await ctx.send(f"{result.parent.get_text()}\n[Read More](https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part})") + else: + await ctx.send("No such rule") + frcrule.example_usage = """ - `{prefix}frcrule G301` - sends a link to rule G301 + `{prefix}frcrule g301` - sends the summary and link to rule G301 """ async def setup(bot): From e499b5f2b29df9e1f72a94e4951b56b7201cf402 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Tue, 21 Nov 2023 19:42:41 +0000 Subject: [PATCH 45/66] use embed --- dozer/cogs/firstqa.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index f73eec1c..2e71da59 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -107,9 +107,19 @@ async def frcqa(self, ctx: DozerContext, question: int): async def frcrule(self, ctx: DozerContext, rule: str): letter_part = ''.join([char for char in rule if char.isalpha()]) number_part = ''.join([char for char in rule if char.isdigit()]) + embed = discord.Embed( + title=f"Rule {letter_part.upper()}{number_part}", + url=f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}", + color=discord.Color.blue() + ) + if not re.match(r'^[a-zA-Z]\d{3}$', rule): - await ctx.send("Invalid rule number") + + embed.add_field( + name="Error", + value="Invalid rule number" + ) else: async with ctx.cog.ses.get('https://firstfrc.blob.core.windows.net/frc2023/Manual/HTML/2023FRCGameManual.htm') as response: @@ -118,10 +128,18 @@ async def frcrule(self, ctx: DozerContext, rule: str): result = ruleSoup.find("a", attrs={"name": f"{letter_part.upper()}{number_part}"}) if result is not None: - await ctx.send(f"{result.parent.get_text()}\n[Read More](https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part})") + embed.add_field( + name="Summary", + value=result.parent.get_text() + ) + else: - await ctx.send("No such rule") + embed.add_field( + name="Error", + value="No such rule" + ) + await ctx.send(embed=embed) frcrule.example_usage = """ `{prefix}frcrule g301` - sends the summary and link to rule G301 """ From b17b9f4f00e9ed112cf8201eb562ee55ed811d6a Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Tue, 21 Nov 2023 20:52:03 +0000 Subject: [PATCH 46/66] fix easy things --- .gitpod.yml | 10 ---------- dozer/cogs/firstqa.py | 3 +++ 2 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 .gitpod.yml diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index b1c4d2ba..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,10 +0,0 @@ -# This configuration file was automatically generated by Gitpod. -# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) -# and commit this file to your remote git repository to share the goodness with others. - -# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart - -tasks: - - init: pip install -r requirements.txt - - diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 2e71da59..865204ca 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -105,6 +105,9 @@ async def frcqa(self, ctx: DozerContext, question: int): @bot_has_permissions(embed_links = True) @app_commands.describe(rule = "The rule number") async def frcrule(self, ctx: DozerContext, rule: str): + """ + Shows rules from a rule number + """ letter_part = ''.join([char for char in rule if char.isalpha()]) number_part = ''.join([char for char in rule if char.isdigit()]) embed = discord.Embed( From 8b609e07cc29c31de3d1a1ba18d1de3e79a80a7b Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Wed, 22 Nov 2023 02:00:55 +0000 Subject: [PATCH 47/66] fix reviews --- dozer/cogs/firstqa.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 865204ca..8985ad8d 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -1,6 +1,7 @@ """Provides commands that pull information from First Q&A Form.""" from typing import Union import re +import datetime import aiohttp from bs4 import BeautifulSoup @@ -108,16 +109,14 @@ async def frcrule(self, ctx: DozerContext, rule: str): """ Shows rules from a rule number """ - letter_part = ''.join([char for char in rule if char.isalpha()]) - number_part = ''.join([char for char in rule if char.isdigit()]) + matches = re.match(r'^(?P[a-zA-Z])(?P\d{3})$', rule) + embed = discord.Embed( - title=f"Rule {letter_part.upper()}{number_part}", - url=f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}", + title=f"Error", color=discord.Color.blue() ) - - if not re.match(r'^[a-zA-Z]\d{3}$', rule): + if matches is None: embed.add_field( name="Error", @@ -125,12 +124,21 @@ async def frcrule(self, ctx: DozerContext, rule: str): ) else: - async with ctx.cog.ses.get('https://firstfrc.blob.core.windows.net/frc2023/Manual/HTML/2023FRCGameManual.htm') as response: + letter_part = matches.group('letter') + number_part = matches.group('number') + current_year = datetime.datetime.now().year + 1 + async with ctx.cog.ses.get(f'https://firstfrc.blob.core.windows.net/frc{current_year}/Manual/HTML/{current_year}FRCGameManual.htm') as response: html_data = await response.content.read() + ruleSoup = BeautifulSoup(html_data, 'html.parser') result = ruleSoup.find("a", attrs={"name": f"{letter_part.upper()}{number_part}"}) if result is not None: + embed = discord.Embed( + title=f"Rule {letter_part.upper()}{number_part}", + url=f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}", + color=discord.Color.blue() + ) embed.add_field( name="Summary", value=result.parent.get_text() From 94574fdf4629cbadfae3f0e77c9d26204a4e9867 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Wed, 22 Nov 2023 02:03:11 +0000 Subject: [PATCH 48/66] ephemeral --- dozer/cogs/firstqa.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 8985ad8d..69e8983c 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -110,6 +110,7 @@ async def frcrule(self, ctx: DozerContext, rule: str): Shows rules from a rule number """ matches = re.match(r'^(?P[a-zA-Z])(?P\d{3})$', rule) + ephemeral = False embed = discord.Embed( title=f"Error", @@ -117,7 +118,7 @@ async def frcrule(self, ctx: DozerContext, rule: str): ) if matches is None: - + ephemeral = True embed.add_field( name="Error", value="Invalid rule number" @@ -145,12 +146,13 @@ async def frcrule(self, ctx: DozerContext, rule: str): ) else: + ephemeral = True embed.add_field( name="Error", value="No such rule" ) - await ctx.send(embed=embed) + await ctx.send(embed=embed, ephemeral=ephemeral) frcrule.example_usage = """ `{prefix}frcrule g301` - sends the summary and link to rule G301 """ From 9b6cbeb2f9f687315f5205997b92d8dca091cd1a Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Wed, 22 Nov 2023 12:16:05 +0000 Subject: [PATCH 49/66] Fix lint errors --- dozer/cogs/firstqa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 69e8983c..0a74e6fb 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -113,7 +113,7 @@ async def frcrule(self, ctx: DozerContext, rule: str): ephemeral = False embed = discord.Embed( - title=f"Error", + title="Error", color=discord.Color.blue() ) @@ -127,8 +127,8 @@ async def frcrule(self, ctx: DozerContext, rule: str): else: letter_part = matches.group('letter') number_part = matches.group('number') - current_year = datetime.datetime.now().year + 1 - async with ctx.cog.ses.get(f'https://firstfrc.blob.core.windows.net/frc{current_year}/Manual/HTML/{current_year}FRCGameManual.htm') as response: + year = datetime.datetime.now().year + 1 + async with ctx.cog.ses.get(f'https://firstfrc.blob.core.windows.net/frc{year}/Manual/HTML/{year}FRCGameManual.htm') as response: html_data = await response.content.read() ruleSoup = BeautifulSoup(html_data, 'html.parser') From a070d62768bc040aed41039e670395f4f4149f53 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Wed, 22 Nov 2023 14:57:36 +0000 Subject: [PATCH 50/66] make newlines nice --- dozer/cogs/firstqa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 0a74e6fb..eec5b0b5 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -127,7 +127,7 @@ async def frcrule(self, ctx: DozerContext, rule: str): else: letter_part = matches.group('letter') number_part = matches.group('number') - year = datetime.datetime.now().year + 1 + year = datetime.datetime.now().year async with ctx.cog.ses.get(f'https://firstfrc.blob.core.windows.net/frc{year}/Manual/HTML/{year}FRCGameManual.htm') as response: html_data = await response.content.read() @@ -142,7 +142,7 @@ async def frcrule(self, ctx: DozerContext, rule: str): ) embed.add_field( name="Summary", - value=result.parent.get_text() + value=' '.join(result.parent.get_text().splitlines()) ) else: From 4cf75d68a0396bb9d2499c9e547524f009fe3d0f Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 27 Nov 2023 22:55:54 -0600 Subject: [PATCH 51/66] Major change in context menu, shouldn't be breaking. Added a new option (onteam) that integrates with the existing onteam functionality to show all teams a user is on --- dozer/cogs/profile_menus.py | 123 ++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 34 deletions(-) diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py index 8e919864..7864eade 100644 --- a/dozer/cogs/profile_menus.py +++ b/dozer/cogs/profile_menus.py @@ -2,6 +2,12 @@ import discord from discord.ext import commands from discord import app_commands +from discord.utils import escape_markdown + + +import db + +embed_color = discord.Color(0xed791e) class ProfileMenus(commands.Cog): @@ -10,42 +16,91 @@ class ProfileMenus(commands.Cog): """ def __init__(self, bot: commands.Bot) -> None: self.bot = bot - self.ctx_menu = app_commands.ContextMenu( - name = 'View Profile', - callback = self.profile, # sets the callback the view_profile function - ) - self.bot.tree.add_command(self.ctx_menu) # add the context menu to the tree - - async def cog_unload(self) -> None: - self.bot.tree.remove_command(self.ctx_menu.name, type = self.ctx_menu.type) - - async def profile(self, interaction: discord.Interaction, member: discord.Member): - """Creates the ephemeral response that will be sent to the user when they interact with the 'View Profile' button""" - # await interaction.response.send_message(f'{member} joined at {discord.utils.format_dt(member.joined_at)}', - # ephemeral = False) # temp false for testing - if member is None: - member = interaction.user - - icon_url = member.avatar.replace(static_format = 'png', size = 32) or None - - embed = discord.Embed(title = member.display_name, description = f'{member!s} ({member.id}) | {member.mention}', - color = member.color) - embed.add_field(name = 'Bot Created' if member.bot else 'Account Created', - value = discord.utils.format_dt(member.created_at), inline = True) - embed.add_field(name = 'Member Joined', value = discord.utils.format_dt(member.joined_at), inline = True) - if member.premium_since is not None: - embed.add_field(name = 'Member Boosted', value = discord.utils.format_dt(member.premium_since), - inline = True) - if len(member.roles) > 1: - role_string = ' '.join([r.mention for r in member.roles][1:]) - else: - role_string = member.roles[0].mention - s = "s" if len(member.roles) >= 2 else "" - embed.add_field(name = f"Role{s}: ", value = role_string, inline = False) - embed.set_thumbnail(url = icon_url) - await interaction.response.send_message(embed = embed, ephemeral = True) + bot.tree.add_command(profile) + bot.tree.add_command(onteam) + + +@app_commands.context_menu(name = 'View Profile') +async def profile(interaction: discord.Interaction, member: discord.Member): + """Creates the ephemeral response that will be sent to the user when they interact with the 'View Profile' button""" + # await interaction.response.send_message(f'{member} joined at {discord.utils.format_dt(member.joined_at)}', + # ephemeral = False) # temp false for testing + if member is None: + member = interaction.user + + icon_url = member.avatar.replace(static_format = 'png', size = 32) or None + + embed = discord.Embed(title = member.display_name, description = f'{member!s} ({member.id}) | {member.mention}', + color = member.color) + embed.add_field(name = 'Bot Created' if member.bot else 'Account Created', + value = discord.utils.format_dt(member.created_at), inline = True) + embed.add_field(name = 'Member Joined', value = discord.utils.format_dt(member.joined_at), inline = True) + if member.premium_since is not None: + embed.add_field(name = 'Member Boosted', value = discord.utils.format_dt(member.premium_since), + inline = True) + if len(member.roles) > 1: + role_string = ' '.join([r.mention for r in member.roles][1:]) + else: + role_string = member.roles[0].mention + s = "s" if len(member.roles) >= 2 else "" + embed.add_field(name = f"Role{s}: ", value = role_string, inline = False) + embed.set_thumbnail(url = icon_url) + await interaction.response.send_message(embed = embed, ephemeral = True) + + +@app_commands.context_menu(name = 'Teams User is On') +async def onteam(interaction: discord.Interaction, user: discord.Member): + """Creates the ephemeral response that will be sent to the user when they interact with the 'View Team' button""" + teams = await TeamNumbers.get_by(user_id = user.id) + + if len(teams) == 0: + await interaction.response.send_message("This user is not on any teams", ephemeral = True) + else: + embed = discord.Embed(color = discord.Color.blue()) + embed.title = f'{user.display_name} is on the following team(s):' + embed.description = "Teams: \n" + for i in teams: + embed.description = f"{embed.description} {i.team_type.upper()} Team {i.team_number} \n" + if len(embed.description) > 4000: + embed.description = embed.description[:4000] + "..." + await interaction.response.send_message(embed = embed, ephemeral = False) async def setup(bot): """Adds the profile context menus cog to the bot.""" await bot.add_cog(ProfileMenus(bot)) + + +class TeamNumbers(db.DatabaseTable): + """Database operations for tracking team associations.""" + __tablename__ = 'team_numbers' + __uniques__ = ('user_id', 'team_number', 'team_type',) + + @classmethod + async def initial_create(cls): + """Create the table in the database""" + async with db.Pool.acquire() as conn: + await conn.execute(f""" + CREATE TABLE {cls.__tablename__} ( + user_id bigint NOT NULL, + team_number text NOT NULL, + team_type VARCHAR NOT NULL, + PRIMARY KEY (user_id, team_number, team_type) + )""") + + def __init__(self, user_id, team_number, team_type): + super().__init__() + self.user_id = user_id + self.team_number = team_number + self.team_type = team_type + + @classmethod + async def get_by(cls, **kwargs): + results = await super().get_by(**kwargs) + result_list = [] + for result in results: + obj = TeamNumbers(user_id = result.get("user_id"), + team_number = result.get("team_number"), + team_type = result.get("team_type")) + result_list.append(obj) + return result_list \ No newline at end of file From 3ae6f80e1a5a24d073a0d771a66ebf2dbc7c812b Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 27 Nov 2023 22:59:48 -0600 Subject: [PATCH 52/66] Forgot my local dozer has a lesser file structure --- dozer/cogs/profile_menus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py index 7864eade..5cf6fb3d 100644 --- a/dozer/cogs/profile_menus.py +++ b/dozer/cogs/profile_menus.py @@ -5,7 +5,7 @@ from discord.utils import escape_markdown -import db +from .. import db embed_color = discord.Color(0xed791e) From d74cb9ed5bb978eb500a573237118446dcb6e41d Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:02:57 -0600 Subject: [PATCH 53/66] I hate lint --- dozer/cogs/profile_menus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py index 5cf6fb3d..bb9b11ec 100644 --- a/dozer/cogs/profile_menus.py +++ b/dozer/cogs/profile_menus.py @@ -103,4 +103,5 @@ async def get_by(cls, **kwargs): team_number = result.get("team_number"), team_type = result.get("team_type")) result_list.append(obj) - return result_list \ No newline at end of file + return result_list + \ No newline at end of file From 9230908f508416abe6c94639b9719dd54c9e1b4b Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Mon, 4 Dec 2023 01:21:29 +0000 Subject: [PATCH 54/66] make newlines nice --- dozer/cogs/firstqa.py | 59 ++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index eec5b0b5..7f9574bc 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -5,6 +5,7 @@ import aiohttp from bs4 import BeautifulSoup +import json import discord from discord.ext import commands from discord import app_commands @@ -58,6 +59,21 @@ async def data(ctx: DozerContext, level: str, question: int) -> Union[str, None] else: return f"That question was not answered or does not exist.\n{forum_url + str(question)}" +def createRuleEmbed(rulenumber, text): + year = datetime.datetime.now().year + embed = discord.Embed( + title=f"Rule {rulenumber}", + url=f"https://rules-search.pages.dev/{year}/rule/{rulenumber}", + color=discord.Color.blue() + ) + + truncated_text = "```\n" + ' '.join(text[:1016].splitlines()) + "```" + embed.add_field( + name="Summary", + value=truncated_text + ) + return embed + class QA(commands.Cog): """QA commands""" @@ -118,32 +134,35 @@ async def frcrule(self, ctx: DozerContext, rule: str): ) if matches is None: - ephemeral = True - embed.add_field( - name="Error", - value="Invalid rule number" - ) + await ctx.defer() + async with ctx.cog.ses.post(f'https://search.grahamsh.com/search',json={'query': rule}) as response: + json_data = await response.content.read() + print(json_data) + json_parsed = json.loads(json_data) + + if "error" not in json_parsed: + embeds = [] + page = 1 + for rule in json_parsed["data"]: + currEmbed = createRuleEmbed(rule["text"], rule["textContent"]) + currEmbed.set_footer(text=f"Page {page} of 5") + embeds.append(currEmbed) + + await paginate(ctx, embeds) + return else: letter_part = matches.group('letter') number_part = matches.group('number') year = datetime.datetime.now().year - async with ctx.cog.ses.get(f'https://firstfrc.blob.core.windows.net/frc{year}/Manual/HTML/{year}FRCGameManual.htm') as response: - html_data = await response.content.read() + async with ctx.cog.ses.get(f'https://rules-search.pages.dev/api/rule?query={letter_part}{number_part}') as response: + json_data = await response.content.read() - ruleSoup = BeautifulSoup(html_data, 'html.parser') - - result = ruleSoup.find("a", attrs={"name": f"{letter_part.upper()}{number_part}"}) - if result is not None: - embed = discord.Embed( - title=f"Rule {letter_part.upper()}{number_part}", - url=f"https://frc-qa.firstinspires.org/manual/rule/{letter_part.upper()}/{number_part}", - color=discord.Color.blue() - ) - embed.add_field( - name="Summary", - value=' '.join(result.parent.get_text().splitlines()) - ) + json_parsed = json.loads(json_data) + + if "error" not in json_parsed: + text = json_parsed["textContent"] + embed = createRuleEmbed(letter_part.upper() + number_part, text) else: ephemeral = True From d931c5e1eda04c83f56b9b49e63eb887f8c4613f Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Mon, 4 Dec 2023 01:26:27 +0000 Subject: [PATCH 55/66] update docs and fix spaces on normal commands --- dozer/cogs/firstqa.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index 7f9574bc..f244d914 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -121,9 +121,9 @@ async def frcqa(self, ctx: DozerContext, question: int): @commands.hybrid_command(name = "frcrule", pass_context = True) @bot_has_permissions(embed_links = True) @app_commands.describe(rule = "The rule number") - async def frcrule(self, ctx: DozerContext, rule: str): + async def frcrule(self, ctx: DozerContext, *, rule: str): """ - Shows rules from a rule number + Shows rules from a rule number or search query """ matches = re.match(r'^(?P[a-zA-Z])(?P\d{3})$', rule) ephemeral = False @@ -137,7 +137,6 @@ async def frcrule(self, ctx: DozerContext, rule: str): await ctx.defer() async with ctx.cog.ses.post(f'https://search.grahamsh.com/search',json={'query': rule}) as response: json_data = await response.content.read() - print(json_data) json_parsed = json.loads(json_data) if "error" not in json_parsed: @@ -174,6 +173,8 @@ async def frcrule(self, ctx: DozerContext, rule: str): await ctx.send(embed=embed, ephemeral=ephemeral) frcrule.example_usage = """ `{prefix}frcrule g301` - sends the summary and link to rule G301 + `{prefix}frcrule can i cross the line before teleop` - sends the summary and link to the rule matching the query + """ async def setup(bot): From df8472070777ac32ab9a5b65b8cd7ddfaa866b05 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Mon, 4 Dec 2023 01:30:05 +0000 Subject: [PATCH 56/66] fix lints --- dozer/cogs/firstqa.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index f244d914..4e33fcb9 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -2,10 +2,10 @@ from typing import Union import re import datetime +import json import aiohttp from bs4 import BeautifulSoup -import json import discord from discord.ext import commands from discord import app_commands @@ -60,6 +60,7 @@ async def data(ctx: DozerContext, level: str, question: int) -> Union[str, None] return f"That question was not answered or does not exist.\n{forum_url + str(question)}" def createRuleEmbed(rulenumber, text): + """Returns an embed for a given rule number and text""" year = datetime.datetime.now().year embed = discord.Embed( title=f"Rule {rulenumber}", @@ -135,15 +136,15 @@ async def frcrule(self, ctx: DozerContext, *, rule: str): if matches is None: await ctx.defer() - async with ctx.cog.ses.post(f'https://search.grahamsh.com/search',json={'query': rule}) as response: + async with ctx.cog.ses.post('https://search.grahamsh.com/search',json={'query': rule}) as response: json_data = await response.content.read() json_parsed = json.loads(json_data) if "error" not in json_parsed: embeds = [] page = 1 - for rule in json_parsed["data"]: - currEmbed = createRuleEmbed(rule["text"], rule["textContent"]) + for currRule in json_parsed["data"]: + currEmbed = createRuleEmbed(currRule["text"], currRule["textContent"]) currEmbed.set_footer(text=f"Page {page} of 5") embeds.append(currEmbed) From 1ceed36c4f8b35d63add958044c3ef502ed42922 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Tue, 5 Dec 2023 17:48:33 +0000 Subject: [PATCH 57/66] fix invite command --- dozer/cogs/firstqa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dozer/cogs/firstqa.py b/dozer/cogs/firstqa.py index eec5b0b5..2b810ecc 100644 --- a/dozer/cogs/firstqa.py +++ b/dozer/cogs/firstqa.py @@ -67,7 +67,7 @@ def __init__(self, bot) -> None: super().__init__() self.bot = bot - @commands.hybrid_command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) + @command(name="ftcqa", aliases=["ftcqaforum"], pass_context=True) @bot_has_permissions(embed_links=True) @app_commands.describe(question="The number of the question you want to look up") async def ftcqa(self, ctx: DozerContext, question: int): @@ -84,7 +84,7 @@ async def ftcqa(self, ctx: DozerContext, question: int): `{prefix}ftcqa 19` - show information on FTC Q&A #19 """ - @commands.hybrid_command(name = "frcqa", aliases = ["frcqaforum"], pass_context = True) + @command(name = "frcqa", aliases = ["frcqaforum"], pass_context = True) @bot_has_permissions(embed_links = True) @app_commands.describe(question = "The number of the question you want to look up") async def frcqa(self, ctx: DozerContext, question: int): @@ -102,7 +102,7 @@ async def frcqa(self, ctx: DozerContext, question: int): """ - @commands.hybrid_command(name = "frcrule", pass_context = True) + @command(name = "frcrule", pass_context = True) @bot_has_permissions(embed_links = True) @app_commands.describe(rule = "The rule number") async def frcrule(self, ctx: DozerContext, rule: str): From a94c59d9b3e0eee2250767832394dc17fff4a778 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Thu, 7 Dec 2023 02:40:27 +0000 Subject: [PATCH 58/66] Fix checkrolelevels slash command --- dozer/cogs/levels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dozer/cogs/levels.py b/dozer/cogs/levels.py index 151b52ef..0500b688 100644 --- a/dozer/cogs/levels.py +++ b/dozer/cogs/levels.py @@ -341,6 +341,7 @@ async def checkrolelevels(self, ctx: DozerContext): unsorted = self._level_roles.get(ctx.guild.id) embed = discord.Embed(title=f"Level roles for {ctx.guild}", color=blurple) if unsorted: + await ctx.defer() roles = sorted(unsorted, key=lambda entry: entry.level) # Sort roles based on level embeds = [] From 134ff5fc5e3a25059b1fb02264b3effea56e561e Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Sun, 31 Dec 2023 17:11:09 +0000 Subject: [PATCH 59/66] start modmail with user --- dozer/cogs/modmail.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index 290e0ec1..b8c1476f 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -35,8 +35,17 @@ class StartModmailModal(ui.Modal): subject = ui.TextInput(label='Subject', custom_id="subject") message = ui.TextInput(label='Message', style=discord.TextStyle.paragraph, custom_id="message") + def __init__(self, *args, **kwargs): + subject = ui.TextInput(label='Subject', custom_id="subject") + + super().__init__(title="New Modmail") + for key, value in kwargs.items(): + setattr(self, key, value) + async def on_submit(self, interaction: discord.Interaction): # pylint: disable=arguments-differ """Handles when a modal is submitted""" + user = self.custom_user if hasattr(self, "custom_user") else interaction.user + subject = interaction.data['components'][0]['components'][0]['value'] message = interaction.data['components'][1]['components'][0]['value'] @@ -49,16 +58,16 @@ async def on_submit(self, interaction: discord.Interaction): # pylint: disable= timestamp=datetime.datetime.utcnow(), ) new_ticket_embed.set_footer( - text=f"{interaction.user.name}" - f"{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} " - f"| {interaction.user.id}", - icon_url=interaction.user.avatar.url if interaction.user.avatar is not None else None, + text=f"{user.name}" + f"{'#' + user.discriminator if user.discriminator != '0' else ''} " + f"| {user.id}", + icon_url=user.avatar.url if user.avatar is not None else None, ) target_record = await ModmailConfig.get_by(guild_id=interaction.guild_id) mod_channel = interaction.client.get_channel(target_record[0].target_channel) - user_string = f"{interaction.user.name}" \ - f"{'#' + interaction.user.discriminator if interaction.user.discriminator != '0' else ''} " \ - f"({interaction.user.id})" + user_string = f"{user.name}" \ + f"{'#' + user.discriminator if user.discriminator != '0' else ''} " \ + f"({user.id})" if len(user_string) > 100: user_string = user_string[:96] + "..." mod_message = await mod_channel.send(user_string) @@ -67,7 +76,7 @@ async def on_submit(self, interaction: discord.Interaction): # pylint: disable= await interaction.response.send_message("Creating private modmail thread!", ephemeral=True) user_thread = await interaction.channel.create_thread(name=subject) - await user_thread.add_user(interaction.user) + await user_thread.add_user(user) await user_thread.join() await user_thread.send(embed=new_ticket_embed) thread_record = ModmailThreads(user_thread=user_thread.id, mod_thread=mod_thread.id) @@ -149,6 +158,17 @@ async def configure_modmail(self, ctx: DozerContext, target_channel): await config.update_or_add() await ctx.reply("Configuration saved!") + @command() + @has_permissions(manage_roles=True) + async def start_modmail_with_user(self, ctx: DozerContext, member: discord.Member): + """Start modmail with a user, should be used in channel with modmail button""" + target_record = await ModmailConfig.get_by(guild_id=ctx.interaction.guild_id) + if len(target_record) == 0: + await ctx.reply("Sorry, this server has not configured modmail correctly yet!", ephemeral=True) + else: + await ctx.interaction.response.send_modal(StartModmailModal(custom_user=member)) + + @command() @has_permissions(administrator=True) async def create_modmail_button(self, ctx): From 721958135c0680432fdff9dbee222324ad977ab2 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Sun, 31 Dec 2023 17:12:34 +0000 Subject: [PATCH 60/66] boop --- dozer/cogs/modmail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index b8c1476f..f360f03b 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -36,7 +36,6 @@ class StartModmailModal(ui.Modal): message = ui.TextInput(label='Message', style=discord.TextStyle.paragraph, custom_id="message") def __init__(self, *args, **kwargs): - subject = ui.TextInput(label='Subject', custom_id="subject") super().__init__(title="New Modmail") for key, value in kwargs.items(): From 741062bc964b26a4815d49de579f324dccd35674 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Sun, 31 Dec 2023 17:16:06 +0000 Subject: [PATCH 61/66] fix lint --- dozer/cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index f360f03b..3c57cb96 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -163,7 +163,7 @@ async def start_modmail_with_user(self, ctx: DozerContext, member: discord.Membe """Start modmail with a user, should be used in channel with modmail button""" target_record = await ModmailConfig.get_by(guild_id=ctx.interaction.guild_id) if len(target_record) == 0: - await ctx.reply("Sorry, this server has not configured modmail correctly yet!", ephemeral=True) + await ctx.reply("Sorry, this server has not configured modmail correctly yet!", ephemeral=True) else: await ctx.interaction.response.send_modal(StartModmailModal(custom_user=member)) From ae092c157b8230ad24ec5a43159eda2590b61ff6 Mon Sep 17 00:00:00 2001 From: Stephan K <55259393+skruglov2023@users.noreply.github.com> Date: Sun, 31 Dec 2023 23:57:41 -0600 Subject: [PATCH 62/66] this should be the resolution of the comment --- dozer/cogs/profile_menus.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dozer/cogs/profile_menus.py b/dozer/cogs/profile_menus.py index bb9b11ec..e9d58ed5 100644 --- a/dozer/cogs/profile_menus.py +++ b/dozer/cogs/profile_menus.py @@ -19,6 +19,10 @@ def __init__(self, bot: commands.Bot) -> None: bot.tree.add_command(profile) bot.tree.add_command(onteam) + async def cog_unload(self) -> None: + self.bot.tree.remove_command(profile) + self.bot.tree.remove_command(onteam) + @app_commands.context_menu(name = 'View Profile') async def profile(interaction: discord.Interaction, member: discord.Member): From 0e51e6d431e75dc3c74714b9274e3ca66139e128 Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Mon, 1 Jan 2024 16:29:22 +0000 Subject: [PATCH 63/66] force slash --- dozer/cogs/modmail.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index 3c57cb96..ea52c0e1 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -161,11 +161,15 @@ async def configure_modmail(self, ctx: DozerContext, target_channel): @has_permissions(manage_roles=True) async def start_modmail_with_user(self, ctx: DozerContext, member: discord.Member): """Start modmail with a user, should be used in channel with modmail button""" - target_record = await ModmailConfig.get_by(guild_id=ctx.interaction.guild_id) - if len(target_record) == 0: - await ctx.reply("Sorry, this server has not configured modmail correctly yet!", ephemeral=True) - else: - await ctx.interaction.response.send_modal(StartModmailModal(custom_user=member)) + if (ctx.interaction is not None): + target_record = await ModmailConfig.get_by(guild_id=ctx.interaction.guild_id) + if len(target_record) == 0: + await ctx.reply("Sorry, this server has not configured modmail correctly yet!", ephemeral=True) + else: + await ctx.interaction.response.send_modal(StartModmailModal(custom_user=member)) + else: + await ctx.reply("This command only works via slash command") + @command() From ec9e7e762ea3fedc7520080307807d826736c0ce Mon Sep 17 00:00:00 2001 From: GrahamSH Date: Mon, 1 Jan 2024 16:35:20 +0000 Subject: [PATCH 64/66] fix lint --- dozer/cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dozer/cogs/modmail.py b/dozer/cogs/modmail.py index ea52c0e1..dcafdae4 100644 --- a/dozer/cogs/modmail.py +++ b/dozer/cogs/modmail.py @@ -161,7 +161,7 @@ async def configure_modmail(self, ctx: DozerContext, target_channel): @has_permissions(manage_roles=True) async def start_modmail_with_user(self, ctx: DozerContext, member: discord.Member): """Start modmail with a user, should be used in channel with modmail button""" - if (ctx.interaction is not None): + if ctx.interaction is not None: target_record = await ModmailConfig.get_by(guild_id=ctx.interaction.guild_id) if len(target_record) == 0: await ctx.reply("Sorry, this server has not configured modmail correctly yet!", ephemeral=True) From 04f2c496c92c0bfef6b78db16e932470cd190f03 Mon Sep 17 00:00:00 2001 From: Silvana <66652216+silvanathecat@users.noreply.github.com> Date: Sat, 6 Jan 2024 02:11:23 +0000 Subject: [PATCH 65/66] Fixed starboard config --- dozer/cogs/starboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dozer/cogs/starboard.py b/dozer/cogs/starboard.py index df554b62..424fd439 100644 --- a/dozer/cogs/starboard.py +++ b/dozer/cogs/starboard.py @@ -39,7 +39,7 @@ def make_starboard_embed(msg: discord.Message, reaction_count: int): """Makes a starboard embed.""" e = discord.Embed(color=msg.author.color, title=f"New Starred Message in #{msg.channel.name}", description=msg.content, url=msg.jump_url) - e.set_author(name=escape_markdown(msg.author.display_name), icon_url=msg.author.display_avatar) + e.set_author(name=escape_markdown(msg.author.display_name.replace('\\', '')), icon_url=msg.author.display_avatar) view_link = f" [[view]]({msg.jump_url})" e.add_field(name="Link:", value=view_link) @@ -254,7 +254,7 @@ async def showconfig(self, ctx: DozerContext): @bot_has_permissions(add_reactions=True, embed_links=True) @starboard.command() async def config(self, ctx: DozerContext, channel: discord.TextChannel, - star_emoji: discord.Emoji, + star_emoji, threshold: int, cancel_emoji: discord.Emoji = None): """Modify the settings for this server's starboard""" if str(star_emoji) == str(cancel_emoji): From 64e48e4bfd3d82cd9cb14fdeb24f211ce3d54d40 Mon Sep 17 00:00:00 2001 From: Silvana <66652216+silvanathecat@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:42:43 +0000 Subject: [PATCH 66/66] starboard changes but they actually work and dont break the official master fork --- dozer/cogs/starboard.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dozer/cogs/starboard.py b/dozer/cogs/starboard.py index 424fd439..e1dfb54e 100644 --- a/dozer/cogs/starboard.py +++ b/dozer/cogs/starboard.py @@ -39,7 +39,7 @@ def make_starboard_embed(msg: discord.Message, reaction_count: int): """Makes a starboard embed.""" e = discord.Embed(color=msg.author.color, title=f"New Starred Message in #{msg.channel.name}", description=msg.content, url=msg.jump_url) - e.set_author(name=escape_markdown(msg.author.display_name.replace('\\', '')), icon_url=msg.author.display_avatar) + e.set_author(name=escape_markdown(msg.author.display_name).replace('\\', ''), icon_url=msg.author.display_avatar) view_link = f" [[view]]({msg.jump_url})" e.add_field(name="Link:", value=view_link) @@ -255,7 +255,7 @@ async def showconfig(self, ctx: DozerContext): @starboard.command() async def config(self, ctx: DozerContext, channel: discord.TextChannel, star_emoji, - threshold: int, cancel_emoji: discord.Emoji = None): + threshold: int, cancel_emoji = None): """Modify the settings for this server's starboard""" if str(star_emoji) == str(cancel_emoji): await ctx.send("The Star Emoji and Cancel Emoji cannot be the same!") @@ -263,14 +263,15 @@ async def config(self, ctx: DozerContext, channel: discord.TextChannel, for emoji in [emoji for emoji in [star_emoji, cancel_emoji] if emoji is not None]: try: # try adding it to make sure it's a real emoji. This covers both custom emoijs & unicode emojis - await ctx.message.add_reaction(emoji) - await ctx.message.remove_reaction(emoji, ctx.guild.me) + message = await channel.send('Testing Reaction') + await message.add_reaction(emoji) + await message.remove_reaction(emoji, ctx.guild.me) if isinstance(emoji, discord.Emoji) and emoji.guild_id != ctx.guild.id: await ctx.send(f"The emoji {emoji} is a custom emoji not from this server!") return - except discord.HTTPException: + except discord.HTTPException as err: await ctx.send(f"{ctx.author.mention}, bad argument: '{emoji}' is not an emoji, or isn't from a server " - f"{ctx.me.name} is in.") + f"{ctx.me.name} is in, error: {err}") return config = StarboardConfig(guild_id=ctx.guild.id, channel_id=channel.id, star_emoji=str(star_emoji),