From b9727cde24f1eb9788a9bf11babdf7c95dd47958 Mon Sep 17 00:00:00 2001 From: maskduck Date: Wed, 29 May 2024 20:52:18 +0700 Subject: [PATCH 1/5] Migrate to slash cmd --- _orangcbot/__main__.py | 12 ++- _orangcbot/extensions/archwiki.py | 24 +++++- _orangcbot/extensions/converters.py | 13 ++- _orangcbot/extensions/dns.py | 30 ++++++- _orangcbot/extensions/faq.py | 18 +++- _orangcbot/extensions/fun.py | 98 ++++++++++++++++++++-- _orangcbot/extensions/nixwiki.py | 21 +++++ _orangcbot/extensions/nonsense.py | 79 ++++++++++++++++- _orangcbot/extensions/testing_functions.py | 6 ++ 9 files changed, 280 insertions(+), 21 deletions(-) diff --git a/_orangcbot/__main__.py b/_orangcbot/__main__.py index 7e3fc03..13cbd0c 100644 --- a/_orangcbot/__main__.py +++ b/_orangcbot/__main__.py @@ -10,7 +10,8 @@ import nextcord import psycopg2 -from nextcord import Intents +from nextcord import ApplicationError, Intents +from nextcord.ext import application_checks as ac from nextcord.ext import commands, help_commands prefix = "oct/" if os.getenv("TEST") else "oc/" @@ -42,6 +43,15 @@ async def on_command_error( await context.send("Fool") await super().on_command_error(context, error) + async def on_application_command_error( + self, interaction: nextcord.Interaction, exception: ApplicationError + ) -> None: + if isinstance(exception, ac.ApplicationMissingRole): + await interaction.send("Imagine not being a staff") + else: + await interaction.send("Fool") + await super().on_application_command_error(interaction, exception) + bot = OrangcBot( intents=Intents.all(), diff --git a/_orangcbot/extensions/archwiki.py b/_orangcbot/extensions/archwiki.py index 1f082da..81faeed 100644 --- a/_orangcbot/extensions/archwiki.py +++ b/_orangcbot/extensions/archwiki.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Final, Generic, Self, TypeVar import aiohttp import nextcord -import typing_extensions +from nextcord import SlashOption, slash_command from nextcord.ext import commands, menus T = TypeVar("T") @@ -56,6 +56,26 @@ async def archwiki(self, ctx: commands.Context, *, query: str) -> None: l: ArchWikiButtonMenu = ArchWikiButtonMenu(k) await l.start(ctx=ctx) + @nextcord.slash_command(name="archwiki") + async def archwiki_( + self: Self, + interaction: nextcord.Interaction, + query: str = SlashOption( + description="The documentation query to search for", required=True + ), + ): + """Query ArchWiki for a documentation entry.""" + async with aiohttp.ClientSession() as session: + async with session.get( + f"https://wiki.archlinux.org/api.php?action=opensearch&search={query}&limit=20&format=json" + ) as resp: + k = await resp.json() + if len(k[1]) == 0: + await interaction.send("No results foundd, aborting.") + return + l: ArchWikiButtonMenu = ArchWikiButtonMenu(k) + await l.start(interaction=interaction) + def setup(bot: commands.Bot) -> None: bot.add_cog(ArchWiki(bot)) diff --git a/_orangcbot/extensions/converters.py b/_orangcbot/extensions/converters.py index 6e2117f..f58d550 100644 --- a/_orangcbot/extensions/converters.py +++ b/_orangcbot/extensions/converters.py @@ -1,9 +1,12 @@ from __future__ import annotations -from typing import Tuple +from typing import Final, Tuple +from nextcord import Interaction, OptionConverter from nextcord.ext import commands +__all__: Final[Tuple[str]] = ("SubdomainNameConverter", "SlashSubdomainNameConverter") + class SubdomainNameConverter(commands.Converter): async def convert(self, ctx: commands.Context, argument: str) -> str: @@ -16,3 +19,11 @@ async def convert(self, ctx: commands.Context, argument: str) -> str: class RGBColorTupleConverter(commands.Converter): async def convert(self, ctx: commands.Context, argument: str) -> Tuple[str]: return argument.split("-") # type: ignore[reportReturnType] + + +class SlashSubdomainNameConverter(OptionConverter): + async def convert(self, interaction: Interaction, value: str) -> str: + value = value.lower() + if value.endswith(".is-a.dev"): + return value[:-9] + return value diff --git a/_orangcbot/extensions/dns.py b/_orangcbot/extensions/dns.py index a54ecec..cdf1926 100644 --- a/_orangcbot/extensions/dns.py +++ b/_orangcbot/extensions/dns.py @@ -73,7 +73,7 @@ def update_msg(self, msg: nextcord.Message): class DNSView(nextcord.ui.View): if TYPE_CHECKING: - _message: nextcord.Message + _message: nextcord.Message | nextcord.PartialInteractionMessage def __init__(self, url: str, author_id: int): super().__init__(timeout=600) @@ -88,7 +88,9 @@ async def interaction_check(self, interaction: nextcord.Interaction) -> bool: await interaction.send("Fool", ephemeral=True) return False - def update_msg(self, msg: nextcord.Message) -> None: + def update_msg( + self, msg: nextcord.Message | nextcord.PartialInteractionMessage + ) -> None: self._message = msg self.dropdown.update_msg(msg) @@ -117,6 +119,30 @@ async def dig(self, ctx: commands.Context, url: str): msg = await ctx.send(embed=construct_embed(url, answer, "CNAME"), view=k) k.update_msg(msg) + @nextcord.slash_command(name="dig") + async def dig_( + self, + interaction: nextcord.Interaction, + url: str = nextcord.SlashOption( + description="The URL to dig for DNS records. Be sure to remove http or https://", + required=True, + ), + ) -> None: + """Dig an URL for its DNS records. Default to CNAME, if you want other things then please choose in the dropdown provided later.""" + try: + answers = _dnsresolver.resolve(url, "CNAME") + answer = "\n".join([str(ans) for ans in answers]) + except _dnsresolver.NoAnswer: + answer = "NOT FOUND" + except _dnsresolver.NXDOMAIN: + await interaction.send("Domain requested does not exist. Aborting.") + return + k = DNSView(url, interaction.user.id) + msg = await interaction.send( + embed=construct_embed(url, answer, "CNAME"), view=k + ) + k.update_msg(msg) + def setup(bot: commands.Bot) -> None: bot.add_cog(DNS(bot)) diff --git a/_orangcbot/extensions/faq.py b/_orangcbot/extensions/faq.py index 698c2ff..ca2451a 100644 --- a/_orangcbot/extensions/faq.py +++ b/_orangcbot/extensions/faq.py @@ -96,8 +96,8 @@ def __init__(self): super().__init__(placeholder="Select your question.", options=options) self._message: nextcord.Message = None - def update_msg(self, message: nextcord.Message): - self._message: nextcord.Message = message + def update_msg(self, message: nextcord.Message | nextcord.InteractionMessage): + self._message: nextcord.Message | nextcord.InteractionMessage = message async def callback(self, interaction: nextcord.Interaction): await interaction.response.defer() @@ -130,13 +130,25 @@ async def faq(self, ctx: commands.Context): """Show FAQ.""" k = FAQView() embed = nextcord.Embed( - title="Welcome to FAQ", + title="Welcome to FAQ.", description="Click the dropdown below to toggle the questions.", color=nextcord.Color.blurple(), ) m = await ctx.send(embed=embed, view=k) k.update_msg(m) + @nextcord.slash_command(name="faq") + async def faq_slash(self, interaction: nextcord.Interaction) -> None: + """Show FAQ.""" + k = FAQView() + embed = nextcord.Embed( + title="Welcome to FAQ.", + description="Click the dropdown below to toggle the questions.", + color=nextcord.Color.blurple(), + ) + m = await interaction.send(embed=embed, view=k) + k.update_msg(m) # type: ignore[reportArgumentType] + def setup(bot): bot.add_cog(FAQ(bot)) diff --git a/_orangcbot/extensions/fun.py b/_orangcbot/extensions/fun.py index 56fca2c..eef63c7 100644 --- a/_orangcbot/extensions/fun.py +++ b/_orangcbot/extensions/fun.py @@ -3,6 +3,7 @@ import aiohttp import dotenv import nextcord +from nextcord import Interaction, SlashOption from nextcord.ext import commands from psl_dns import PSL @@ -239,17 +240,13 @@ async def moral( if not member: member = ctx.author # type: ignore[reportAssignmentType] if member.id == 716134528409665586: # type: ignore[reportOptionalMemberAccess] - state = "Paragon of Virtue" elif member.id == 853158265466257448: # type: ignore[reportOptionalMemberAccess] - state = "Beneath contempt" elif member.id == 961063229168164864: # type: ignore[reportOptionalMemberAccess] - state = "Degenerate" else: - state = choice(_morals) await ctx.send(f"**{member.display_name}**'s moral status is **{state}**") # type: ignore[reportOptionalMemberAccess] @@ -260,17 +257,13 @@ async def see_moral( ) -> None: # state = "" if member.id == 716134528409665586: - state = "Paragon of Virtue" elif member.id == 853158265466257448: - state = "Beneath contempt" elif member.id == 961063229168164864: - state = "Degenerate" else: - state = choice(_morals) await interaction.response.send_message( f"**{member.display_name}**'s moral status is **{state}**" @@ -323,5 +316,94 @@ async def shouldi(self, ctx: commands.Context, question: Optional[str] = None): await ctx.send(f"answer: [{r['answer']}]({r['image']})") +class FunSlash(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self._bot = bot + + @nextcord.slash_command() + async def dog(self, interaction: Interaction): + k = await request("GET", "https://dog.ceo/api/breeds/image/random") + await interaction.send(k["message"]) + + @nextcord.slash_command() + async def httpcat( + self, + interaction: nextcord.Interaction, + code: int = SlashOption( + description="The HTTP code to fetch for", required=True + ), + ) -> None: + await interaction.send(f"https://http.cat/{code}") + + @nextcord.slash_command() + async def shouldi( + self, + interaction: nextcord.Interaction, + question: str = SlashOption( + description="What are you asking me for?", required=False + ), + ) -> None: + r = await request("GET", "https://yesno.wtf/api") + await interaction.send(f"answer: [{r['answer']}]({r['image']})") + + @nextcord.slash_command() + async def imbored( + self, + interaction: nextcord.Interaction, + ) -> None: + response = await request("GET", "http://www.boredapi.com/api/activity/") + await interaction.send( + f"You should probably **{response['activity']}** to occupy yourself." + ) + + @nextcord.slash_command() + async def ubdict( + self, + interaction: nextcord.Interaction, + word: str = SlashOption(description="The word to search for", required=True), + ) -> None: + params = {"term": word} + async with aiohttp.ClientSession() as session: + async with session.get( + "https://api.urbandictionary.com/v0/define", params=params + ) as response: + data = await response.json() + if not data["list"]: + await interaction.send("No results found.") + return + embed = nextcord.Embed( + title=data["list"][0]["word"], + description=data["list"][0]["definition"], + url=data["list"][0]["permalink"], + color=nextcord.Color.green(), + ) + embed.set_footer( + text=f"👍 {data['list'][0]['thumbs_up']} | 👎 {data['list'][0]['thumbs_down']} | Powered by: Urban Dictionary" + ) + await interaction.send(embed=embed) + + @nextcord.slash_command() + async def moral( + self, + interaction: Interaction, + member: nextcord.User = SlashOption( + description="The user you want to see the moral." + ), + ) -> None: + if member.id == 716134528409665586: + state = "Paragon of Virtue" + elif member.id == 853158265466257448: + state = "Beneath contempt" + elif member.id == 961063229168164864: + state = "Degenerate" + + else: + state = choice(_morals) + await interaction.response.send_message( + f"**{member.display_name}**'s moral status is **{state}**" + ) + + def setup(bot): bot.add_cog(Fun(bot)) + bot.add_cog(FunSlash(bot)) diff --git a/_orangcbot/extensions/nixwiki.py b/_orangcbot/extensions/nixwiki.py index afabad9..ea60821 100644 --- a/_orangcbot/extensions/nixwiki.py +++ b/_orangcbot/extensions/nixwiki.py @@ -53,6 +53,27 @@ async def nixwiki(self, ctx: commands.Context, *, query: str) -> None: l: NixWikiButtonMenu = NixWikiButtonMenu(k) await l.start(ctx=ctx) + @nextcord.slash_command(name="nixwiki") + async def nixwiki_( + self, + interaction: nextcord.Interaction, + query: str = nextcord.SlashOption( + description="The query of the documentation page to search for.", + required=True, + ), + ) -> None: + """Query the NixWiki Documentation for a specified query.""" + async with aiohttp.ClientSession() as session: + async with session.get( + f"https://nixos.wiki/api.php?action=opensearch&search={query}&limit=20&format=json" + ) as resp: + k = await resp.json() + if len(k[1]) == 0: + await interaction.send("No results found") + return + l: NixWikiButtonMenu = NixWikiButtonMenu(k) + await l.start(interaction=interaction) + def setup(bot: commands.Bot) -> None: bot.add_cog(NixWiki(bot)) diff --git a/_orangcbot/extensions/nonsense.py b/_orangcbot/extensions/nonsense.py index 426f3c5..e663276 100644 --- a/_orangcbot/extensions/nonsense.py +++ b/_orangcbot/extensions/nonsense.py @@ -5,9 +5,11 @@ import aiohttp import nextcord +from nextcord import Interaction, SlashOption, slash_command from nextcord.ext import commands -from .converters import SubdomainNameConverter +from .converters import SlashSubdomainNameConverter, SubdomainNameConverter +from .types import Domain class DomainNotExistError(commands.CommandError): @@ -73,14 +75,14 @@ async def callback(self, interaction: nextcord.Interaction): class ProposeView(nextcord.ui.View): if TYPE_CHECKING: - message: nextcord.Message + message: nextcord.Message | nextcord.InteractionMessage def __init__(self, spouse_id: int): super().__init__(timeout=30) self._spouse_id: int = spouse_id self._answered: Optional[bool] = None - def update_msg(self, msg: nextcord.Message): + def update_msg(self, msg: nextcord.Message | nextcord.InteractionMessage): self._message = msg @nextcord.ui.button(label="Yes", style=nextcord.ButtonStyle.green) @@ -179,7 +181,8 @@ async def screenshot(self, ctx: commands.Context, url: str): ) ) - def fetch_description_about_a_domain(self, data: Dict): + @classmethod + def fetch_description_about_a_domain(cls, data: Domain): parsed_contact = {} for platform, username in data["owner"].items(): if platform == "username": @@ -275,5 +278,73 @@ async def check( ) +class NonsenseSlash(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self._bot: commands.Bot = bot + + @nextcord.slash_command() + async def check( + self, + interaction: nextcord.Interaction, + domain: SlashSubdomainNameConverter = SlashOption( + description="The domain name to check for.", required=True + ), + ) -> None: + try: + data = await request( + True, + "GET", + f"https://raw.githubusercontent.com/is-a-dev/register/main/domains/{domain}.json", + ) + await interaction.send(f"Domain {domain} is already taken.") + except DomainNotExistError: + await interaction.send( + "This domain is still available. Claim it before someone take it." + ) + + @slash_command() + async def whois( + self, + interaction: Interaction, + domain: SlashSubdomainNameConverter = SlashOption( + description="The is-a.dev domain name to find whois for.", required=True + ), + ) -> None: + try: + data = await request( + True, + "GET", + f"https://raw.githubusercontent.com/is-a-dev/register/main/domains/{domain}.json", + ) + await interaction.send( + embed=nextcord.Embed( + title=f"Domain info for {domain}.is-a.dev", + description=Nonsense.fetch_description_about_a_domain(data), + color=nextcord.Color.red(), + ) + ) + except DomainNotExistError: + await interaction.send("Domain requested cannot be found. Aborting.") + + @slash_command() + async def report_degenerate(self, interaction: Interaction) -> None: + await interaction.response.send_modal(ReportDegenModal()) + + @slash_command() + async def propose(self, interaction: Interaction) -> None: + k = ProposeView(interaction.user.id) + l = await interaction.send("Will you marry me?", view=k) + k.update_msg(l) + + @slash_command() + async def links(self, interaction: Interaction) -> None: + """Links that are important to this service.""" + k = """Please also check those channels: + <#991779321758896258> (for an interactive experience go to <#960446827579199488> and type `oc/faq`) + <#1228996111390343229> + """ + await interaction.send(k, view=LinkView()) + + def setup(bot: commands.Bot): bot.add_cog(Nonsense(bot)) diff --git a/_orangcbot/extensions/testing_functions.py b/_orangcbot/extensions/testing_functions.py index 872315f..16bf2cd 100644 --- a/_orangcbot/extensions/testing_functions.py +++ b/_orangcbot/extensions/testing_functions.py @@ -24,6 +24,12 @@ async def hinder(self, ctx: commands.Context, cmd: str): async def test_owner_perm(self, ctx: commands.Context): await ctx.send("Master, how can I help you?") + @commands.command() + @commands.is_owner() + async def reload_slash_command(self, ctx: commands.Context) -> None: + await ctx.bot.sync_application_commands() + await ctx.send("Request satisfied, master.") + def setup(bot: commands.Bot) -> None: bot.add_cog(Testings(bot)) From e209c9d1551e2041925424b01e30c9c6d62e4278 Mon Sep 17 00:00:00 2001 From: maskduck Date: Wed, 29 May 2024 20:57:23 +0700 Subject: [PATCH 2/5] boredapi is broken woohoo --- _orangcbot/extensions/fun.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/_orangcbot/extensions/fun.py b/_orangcbot/extensions/fun.py index eef63c7..a556653 100644 --- a/_orangcbot/extensions/fun.py +++ b/_orangcbot/extensions/fun.py @@ -290,14 +290,6 @@ async def fool( await ctx.send(f"**{member.display_name}** is {level}% a fool.") # type: ignore[reportOptionalMemberAccess] - @commands.command() - async def imbored(self, ctx: commands.Context): - """Fetch an activity to do from BoredAPI.""" - response = await request("GET", "http://www.boredapi.com/api/activity/") - await ctx.send( - f"You should probably **{response['activity']}** to occupy yourself." - ) - @commands.command() async def httpcat(self, ctx: commands.Context, code: int = 406): """Fetch an HTTP Cat image from the http.cat API.""" @@ -346,16 +338,6 @@ async def shouldi( r = await request("GET", "https://yesno.wtf/api") await interaction.send(f"answer: [{r['answer']}]({r['image']})") - @nextcord.slash_command() - async def imbored( - self, - interaction: nextcord.Interaction, - ) -> None: - response = await request("GET", "http://www.boredapi.com/api/activity/") - await interaction.send( - f"You should probably **{response['activity']}** to occupy yourself." - ) - @nextcord.slash_command() async def ubdict( self, @@ -387,7 +369,7 @@ async def moral( self, interaction: Interaction, member: nextcord.User = SlashOption( - description="The user you want to see the moral." + description="The user you want to see the moral.", required=False ), ) -> None: if member.id == 716134528409665586: From d9a341462e16f83fb6f6925dc122bcbe026fa9fb Mon Sep 17 00:00:00 2001 From: maskduck Date: Wed, 29 May 2024 20:59:23 +0700 Subject: [PATCH 3/5] should alias the member to interaction.user if not given --- _orangcbot/extensions/fun.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_orangcbot/extensions/fun.py b/_orangcbot/extensions/fun.py index a556653..2c11bd9 100644 --- a/_orangcbot/extensions/fun.py +++ b/_orangcbot/extensions/fun.py @@ -372,6 +372,8 @@ async def moral( description="The user you want to see the moral.", required=False ), ) -> None: + if not member: + member = interaction.user if member.id == 716134528409665586: state = "Paragon of Virtue" elif member.id == 853158265466257448: From a9b7beefba1a3e3c03c8c58d5572af4c9b9a1f32 Mon Sep 17 00:00:00 2001 From: maskduck Date: Wed, 29 May 2024 21:00:45 +0700 Subject: [PATCH 4/5] Imagine not being deployed --- _orangcbot/extensions/types.py | 31 +++++++++++++++++++++++++++++++ _orangcbot/extensions/utils.py | 26 ++++++++++++++++++++++++++ pyrightconfig.json | 5 +++++ 3 files changed, 62 insertions(+) create mode 100644 _orangcbot/extensions/types.py create mode 100644 _orangcbot/extensions/utils.py create mode 100644 pyrightconfig.json diff --git a/_orangcbot/extensions/types.py b/_orangcbot/extensions/types.py new file mode 100644 index 0000000..5e8b923 --- /dev/null +++ b/_orangcbot/extensions/types.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import List, Literal, TypedDict, Union + +from typing_extensions import NotRequired + +RecordType = Literal["A", "CNAME", "MX", "URL", "TXT"] + + +class _OwnerObject(TypedDict): + username: str + email: str + + # known accounts + discord: NotRequired[str] + twitter: NotRequired[str] + + +class _RecordObject(TypedDict, total=False): + A: List[str] + CNAME: str + URL: str + TXT: Union[str, List[str]] + MX: List[str] + + +class Domain(TypedDict): + description: NotRequired[str] + repo: NotRequired[str] + owner: _OwnerObject + record: _RecordObject diff --git a/_orangcbot/extensions/utils.py b/_orangcbot/extensions/utils.py new file mode 100644 index 0000000..327c5c4 --- /dev/null +++ b/_orangcbot/extensions/utils.py @@ -0,0 +1,26 @@ +from __future__ import annotations + + +import nextcord +from nextcord.ui import Item + +from typing import TYPE_CHECKING + + +class TextBasedOnlyAuthorCheck(Item): + if TYPE_CHECKING: + _message: nextcord.Message + _author_id: int + + async def interaction_check(self, interaction: nextcord.Interaction) -> bool: + if hasattr(self, "_message"): + return interaction.user.id == self._message.author.id + elif hasattr(self, "_author_id"): + return interaction.user.id == self._author_id + else: + raise nextcord.ApplicationCheckFailure( + ( + "An `TextBasedOnlyAuthorCheck` was used but" + "neither `_message` or `_author_id` attribute was found." + ) + ) diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..55b8019 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,5 @@ +{ +"reportOptionalMemberAccess": false, +"reportAssignmentType": false, +"reportIncompatibleMethodOverride": false +} From 2a2bba6f6a24c6087be5b231606bf539bf4054c3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 14:03:21 +0000 Subject: [PATCH 5/5] style: auto fixes from pre-commit hooks --- _orangcbot/extensions/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/_orangcbot/extensions/utils.py b/_orangcbot/extensions/utils.py index 327c5c4..5c76a4c 100644 --- a/_orangcbot/extensions/utils.py +++ b/_orangcbot/extensions/utils.py @@ -1,11 +1,10 @@ from __future__ import annotations +from typing import TYPE_CHECKING import nextcord from nextcord.ui import Item -from typing import TYPE_CHECKING - class TextBasedOnlyAuthorCheck(Item): if TYPE_CHECKING: