diff --git a/examples/basic_bot_example.py b/examples/basic_bot_example.py index 9524a2a9..53569a37 100644 --- a/examples/basic_bot_example.py +++ b/examples/basic_bot_example.py @@ -1,57 +1,74 @@ # -*- coding: utf-8 -*- -# Copyright © tandemdude 2020-present +# Copyright (c) 2023-present tandemdude # -# This file is part of Lightbulb. +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# Lightbulb is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Lightbulb is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Lightbulb. If not, see . +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + import hikari import lightbulb -bot = lightbulb.BotApp(prefix="!", token="YOUR_TOKEN", intents=hikari.Intents.ALL_UNPRIVILEGED) +bot = hikari.GatewayBot(token="...") +client = lightbulb.client_from_app(bot) + +bot.subscribe(hikari.StartingEvent, client.start) -@bot.listen(hikari.ShardReadyEvent) -async def ready_listener(_): - print("The bot is ready!") +@client.register() +class Ping( + lightbulb.SlashCommand, + name="ping", + description="Checks that the bot is alive", +): + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + """Checks that the bot is alive""" + await ctx.respond("Pong!") -@bot.command() -@lightbulb.command("ping", "Checks that the bot is alive") -@lightbulb.implements(lightbulb.PrefixCommand) -async def ping(ctx: lightbulb.Context) -> None: - """Checks that the bot is alive""" - await ctx.respond("Pong!") +@client.register() +class Echo( + lightbulb.SlashCommand, + name="echo", + description="Repeats the user's input", +): + text = lightbulb.string("text", "Text to repeat") + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + """Repeats the user's input""" + await ctx.respond(self.text) -@bot.command() -@lightbulb.option("num2", "Second number", int) -@lightbulb.option("num1", "First number", int) -@lightbulb.command("add", "Adds the two given numbers together") -@lightbulb.implements(lightbulb.PrefixCommand) -async def add(ctx: lightbulb.Context) -> None: - """Adds the two given numbers together""" - num1, num2 = ctx.options.num1, ctx.options.num2 - await ctx.respond(f"{num1} + {num2} = {num1 + num2}") +@client.register() +class Add( + lightbulb.SlashCommand, + name="add", + description="Adds the two given numbers together", +): + # Order of options go from top to bottom + num1 = lightbulb.integer("num1", "First number") + num2 = lightbulb.integer("num2", "Second number") -@bot.command() -@lightbulb.option("user", "User to greet", hikari.User) -@lightbulb.command("greet", "Greets the specified user") -@lightbulb.implements(lightbulb.PrefixCommand) -async def greet(ctx: lightbulb.Context) -> None: - await ctx.respond(f"Hello {ctx.options.user.mention}!") + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + """Adds the two given numbers together""" + await ctx.respond(f"{self.num1} + {self.num2} = {self.num1 + self.num2}") bot.run() diff --git a/examples/basic_slash_command_bot_example.py b/examples/basic_slash_command_bot_example.py deleted file mode 100644 index 8caf5378..00000000 --- a/examples/basic_slash_command_bot_example.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © tandemdude 2020-present -# -# This file is part of Lightbulb. -# -# Lightbulb is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Lightbulb is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Lightbulb. If not, see . -import lightbulb - -bot = lightbulb.BotApp(prefix="!", token="YOUR_TOKEN") - - -@bot.command() -@lightbulb.command("ping", "Checks that the bot is alive") -@lightbulb.implements(lightbulb.SlashCommand) -async def ping(ctx: lightbulb.Context) -> None: - """Checks that the bot is alive""" - await ctx.respond("Pong!") - - -@bot.command() -@lightbulb.option("text", "Text to repeat") -@lightbulb.command("echo", "Repeats the user's input") -@lightbulb.implements(lightbulb.SlashCommand) -async def echo(ctx: lightbulb.Context) -> None: - await ctx.respond(ctx.options.text) - - -bot.run() diff --git a/examples/extension_example.py b/examples/extension_example.py index a28ec1f9..2ba1fa89 100644 --- a/examples/extension_example.py +++ b/examples/extension_example.py @@ -1,36 +1,38 @@ # -*- coding: utf-8 -*- -# Copyright © tandemdude 2020-present +# Copyright (c) 2023-present tandemdude # -# This file is part of Lightbulb. +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# Lightbulb is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Lightbulb is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Lightbulb. If not, see . -import lightbulb - -example_plugin = lightbulb.Plugin("ExamplePlugin") +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import lightbulb -@example_plugin.command() -@lightbulb.command("ping", "Checks that the bot is alive") -@lightbulb.implements(lightbulb.PrefixCommand, lightbulb.SlashCommand) -async def ping(ctx: lightbulb.Context) -> None: - """Checks that the bot is alive""" - await ctx.respond("Pong!") - +loader = lightbulb.Loader() -def load(bot): - bot.add_plugin(example_plugin) +@loader.command +class Greet( + lightbulb.SlashCommand, + name="greet", + description="Greets the specified user", +): + user = lightbulb.user("user", "User to greet") -def unload(bot): - bot.remove_plugin(example_plugin) + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context) -> None: + """Greets the specified user""" + await ctx.respond(f"Hello, {self.user.mention}!") diff --git a/examples/in_depth_component_example.py b/examples/in_depth_component_example.py deleted file mode 100644 index 1c79e92c..00000000 --- a/examples/in_depth_component_example.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © tandemdude 2020-present -# -# This file is part of Lightbulb. -# -# Lightbulb is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Lightbulb is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Lightbulb. If not, see . - -import typing as t - -import hikari -from hikari.impl import MessageActionRowBuilder - -import lightbulb - -######################################################################## -# Helper functions and data. -######################################################################## - -# Mapping of color names to hex literal and a fact about the color. -COLORS: t.Mapping[str, t.Tuple[int, str]] = { - "Red": ( - 0xFF0000, - "Due to it's long wavelength, red is the first color a baby sees!", - ), - "Green": ( - 0x00FF00, - "Plants green color help them use photosynthesis!", - ), - "Blue": ( - 0x0000FF, - "Globally, blue is the most common favorite color!", - ), - "Orange": (0xFFA500, "The color orange is named after its fruity counterpart, the orange!"), - "Purple": ( - 0xA020F0, - "Purple is the hardest color for human eyes to distinguish!", - ), - "Yellow": ( - 0xFFFF00, - "Taxi's and school buses are yellow because it's so easy to see!", - ), - "Black": (0x000000, "Black is a color which results from the absence of visible light!"), - "White": (0xFFFFFF, "White objects fully reflect and scatter all visible light!"), -} - - -async def generate_rows(bot: lightbulb.BotApp) -> t.Iterable[MessageActionRowBuilder]: - """Generate 2 action rows with 4 buttons each.""" - - # This will hold our action rows of buttons. The limit - # imposed by Discord is 5 rows with 5 buttons each. We - # will not use that many here, however. - rows: t.List[MessageActionRowBuilder] = [] - - # Build the first action row - row = bot.rest.build_message_action_row() - - # Here we iterate len(COLORS) times. - for i in range(len(COLORS)): - if i % 4 == 0 and i != 0: - # If i is evenly divided by 4, and not 0 we want to - # append the first row to rows and build the second - # action row. (Gives a more even button layout) - rows.append(row) - row = bot.rest.build_message_action_row() - - # Extract the current color from the mapping and assign - # to this label var for later. - label = list(COLORS)[i] - - # We use an enclosing scope here so that we can easily chain - # method calls of the action row. - ( - # Adding the buttons into the action row. - row.add_button( - # Gray button style, see also PRIMARY, and DANGER. - hikari.ButtonStyle.SECONDARY, - # Set the buttons custom ID to the label. - label, - ) - # Set the actual label. - .set_label(label) - # Finally add the button to the container. - .add_to_container() - ) - - # Append the second action row to rows after the for loop. - rows.append(row) - - # Return the action rows from the function. - return rows - - -async def handle_responses( - bot: lightbulb.BotApp, - author: hikari.User, - message: hikari.Message, -) -> None: - """Watches for events, and handles responding to them.""" - - # Now we need to check if the user who ran the command interacts - # with our buttons, we stop watching after 120 seconds (2 mins) of - # inactivity. - with bot.stream(hikari.InteractionCreateEvent, 120).filter( - # Here we filter out events we don't care about. - lambda e: ( - # A component interaction is a button interaction. - isinstance(e.interaction, hikari.ComponentInteraction) - # Make sure the command author hit the button. - and e.interaction.user == author - # Make sure the button was attached to our message. - and e.interaction.message == message - ) - ) as stream: - async for event in stream: - # If we made it through the filter, the user has clicked - # one of our buttons, so we grab the custom ID. - cid = event.interaction.custom_id - - # Create new embed with info on the color they selected - embed = hikari.Embed( - # The color name. - title=cid, - # The hex literal we stored earlier. - color=COLORS[cid][0], - # The fact about the color. - description=COLORS[cid][1], - ) - - # If we haven't responded to the interaction yet, we - # need to create the initial response. Otherwise, we - # need to edit the initial response. - try: - # NOTE: We don't have to add the buttons again as they - # are already on the message. So we don't have to - # pass components here. If we wanted to update the - # buttons we would pass a new list of action rows. - await event.interaction.create_initial_response( - # The response type is required when creating - # the initial response. We use MESSAGE_UPDATE - # because we are updating a message we previously - # sent. NOTE: even though the message was already - # sent, this is still the **INITIAL RESPONSE** to - # the interaction event (button click). - hikari.ResponseType.MESSAGE_UPDATE, - embed=embed, - ) - except hikari.NotFoundError: - # This error is raised if we have already sent the - # initial response. Notice no response type is needed - # here, so we just edit the initial response with the - # new embed. - await event.interaction.edit_initial_response( - embed=embed, - ) - - # Once were back outside the stream loop, it's been 2 minutes since - # the last interaction and it's time now to remove the buttons from - # the message to prevent further interaction. - await message.edit( - # Set components to an empty list to get rid of them. - components=[] - ) - - -######################################################################## -# Create the bot. -######################################################################## - - -# Instantiate the bot. -bot = lightbulb.BotApp(token="YOUR_TOKEN", prefix="!") - - -# Create the message command. -@bot.command() -@lightbulb.command("rgb", "Get facts on different colors!", guilds=[1234]) -@lightbulb.implements(lightbulb.PrefixCommand, lightbulb.SlashCommand) -async def rgb_command(ctx: lightbulb.Context) -> None: - """Get facts on different colors!""" - - # Generate the action rows. - rows = await generate_rows(ctx.bot) - - # Send the initial response with our action rows, and save the - # message for handling interaction responses. - response = await ctx.respond( - hikari.Embed(title="Pick a color"), - components=rows, - ) - message = await response.message() - - # Handle interaction responses to the initial message. - await handle_responses(ctx.bot, ctx.author, message) - - -# Run the bot. -bot.run() diff --git a/examples/moderation_example.py b/examples/moderation_example.py index ff58a86f..deab7978 100644 --- a/examples/moderation_example.py +++ b/examples/moderation_example.py @@ -1,71 +1,97 @@ # -*- coding: utf-8 -*- -# Copyright © tandemdude 2020-present +# Copyright (c) 2023-present tandemdude # -# This file is part of Lightbulb. +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: # -# Lightbulb is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Lightbulb is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Lightbulb. If not, see . +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + import datetime import hikari import lightbulb -bot = lightbulb.BotApp(token="YOUR_TOKEN", intents=hikari.Intents.ALL_UNPRIVILEGED) +bot = hikari.GatewayBot(token="...") +client = lightbulb.client_from_app(bot) + +bot.subscribe(hikari.StartingEvent, client.start) + + +@client.register() +class Ban( + lightbulb.SlashCommand, + name="ban", + description="Bans a user from the server", + dm_enabled=False, + default_member_permissions=hikari.Permissions.BAN_MEMBERS, +): + # Order of options go from top to bottom + user = lightbulb.user("user", "The user to ban") + # Give non-required options a default value (e.g. default=None) + # Non-required options MUST appear after required options + # Required options do not have a default value + reason = lightbulb.string("reason", "Reason for the ban", default=None) + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context, rest: hikari.api.RESTClient) -> None: + """Ban a user from the server with an optional reason""" + if not ctx.guild_id: + await ctx.respond("This command can only be used in a guild.") + return -@bot.command() -@lightbulb.option("reason", "Reason for the ban", required=False) -@lightbulb.option("user", "The user to ban.", type=hikari.User) -@lightbulb.command("ban", "Ban a user from the server.") -@lightbulb.implements(lightbulb.SlashCommand) -async def ban(ctx: lightbulb.SlashContext) -> None: - """Ban a user from the server with an optional reason.""" - if not ctx.guild_id: - await ctx.respond("This command can only be used in a guild.") - return + # Create a deferred response as the ban may take longer than 3 seconds + await ctx.respond(hikari.ResponseType.DEFERRED_MESSAGE_CREATE) + # Perform the ban + await rest.ban_user(ctx.guild_id, self.user.id, reason=self.reason or hikari.UNDEFINED) + # Provide feedback to the moderator + await ctx.respond(f"Banned {self.user.mention}.\n**Reason:** {self.reason or 'No reason provided.'}") - # Create a deferred response as the ban may take longer than 3 seconds - await ctx.respond(hikari.ResponseType.DEFERRED_MESSAGE_CREATE) - # Perform the ban - await ctx.app.rest.ban_user(ctx.guild_id, ctx.options.user.id, reason=ctx.options.reason or hikari.UNDEFINED) - # Provide feedback to the moderator - await ctx.respond(f"Banned {ctx.options.user.mention}.\n**Reason:** {ctx.options.reason or 'No reason provided.'}") +@client.register() +class Purge( + lightbulb.SlashCommand, + name="purge", + description="Purge a certain amount of messages from a channel", + dm_enabled=False, + default_member_permissions=hikari.Permissions.MANAGE_MESSAGES, +): + count = lightbulb.integer("count", "The amount of messages to purge", max_value=100, min_value=1) -@bot.command() -@lightbulb.option("count", "The amount of messages to purge.", type=int, max_value=100, min_value=1) -# You may also use pass_options to pass the options directly to the function -@lightbulb.command("purge", "Purge a certain amount of messages from a channel.", pass_options=True) -@lightbulb.implements(lightbulb.SlashCommand) -async def purge(ctx: lightbulb.SlashContext, count: int) -> None: - """Purge a certain amount of messages from a channel.""" - if not ctx.guild_id: - await ctx.respond("This command can only be used in a server.") - return + @lightbulb.invoke + async def invoke(self, ctx: lightbulb.Context, rest: hikari.api.RESTClient) -> None: + """Purge a certain amount of messages from a channel""" + if not ctx.guild_id: + await ctx.respond("This command can only be used in a server.") + return - # Fetch messages that are not older than 14 days in the channel the command is invoked in - # Messages older than 14 days cannot be deleted by bots, so this is a necessary precaution - messages = ( - await ctx.app.rest.fetch_messages(ctx.channel_id) - .take_until(lambda m: datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=14) > m.created_at) - .limit(count) - ) - if messages: - await ctx.app.rest.delete_messages(ctx.channel_id, messages) - await ctx.respond(f"Purged {len(messages)} messages.") - else: - await ctx.respond("Could not find any messages younger than 14 days!") + # Fetch messages that are not older than 14 days in the channel the command is invoked in + # Messages older than 14 days cannot be deleted by bots, so this is a necessary precaution + messages = ( + await rest.fetch_messages(ctx.channel_id) + .take_until( + lambda m: datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=14) > m.created_at + ) + .limit(self.count) + ) + if messages: + await rest.delete_messages(ctx.channel_id, messages) + await ctx.respond(f"Purged {len(messages)} messages.") + else: + await ctx.respond("Could not find any messages younger than 14 days!") bot.run() diff --git a/examples/plugin_example.py b/examples/plugin_example.py deleted file mode 100644 index d3e67e63..00000000 --- a/examples/plugin_example.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © tandemdude 2020-present -# -# This file is part of Lightbulb. -# -# Lightbulb is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Lightbulb is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Lightbulb. If not, see . -import lightbulb - -bot = lightbulb.BotApp(prefix="!", token="YOUR_TOKEN") - - -plugin = lightbulb.Plugin("Example Plugin") - - -@plugin.command() -@lightbulb.option("text", "Text to repeat", modifier=lightbulb.OptionModifier.CONSUME_REST) -@lightbulb.command("echo", "Repeats the user's input") -@lightbulb.implements(lightbulb.PrefixCommand) -async def echo(ctx: lightbulb.Context) -> None: - await ctx.respond(ctx.options.text) - - -bot.add_plugin(plugin) -bot.run() diff --git a/fragments/436.doc.md b/fragments/436.doc.md new file mode 100644 index 00000000..7985fec4 --- /dev/null +++ b/fragments/436.doc.md @@ -0,0 +1 @@ +Update code examples for version 3 from version 2.