1
votes

Outcome

To snipe messages sent in X channel instead of all the channels within the Discord guild. That is, it should only track message deletions in that one channel (identified by its ID), and only respond to the !snipe command in that same channel. The current code I have here snipes every message sent within the Discord guild.

Question

How can I snipe messages sent in X channel instead of the entire guild?

I mostly intend to run this bot in one guild. However, it would be nice if it could scale to multiple guilds if needed.

The code I have so far is below.

import discord
from discord.ext import commands
from tokens import token

client = commands.Bot(command_prefix="!", self_bot=False)
client.sniped_messages = {}


@client.event
async def on_ready():
    print("Your bot is ready.")


@client.event
async def on_message_delete(message):
    print(f'sniped message {message}')
    client.sniped_messages[message.guild.id] = (
        message.content, message.author, message.channel.name, message.created_at)


@client.command()
async def snipe(ctx):
    try:
        contents, author, channel_name, time = client.sniped_messages[ctx.guild.id]

    except:
        await ctx.channel.send("Couldn't find a message to snipe!")
        return

    embed = discord.Embed(description=contents,
                          color=discord.Color.purple(), timestamp=time)
    embed.set_author(
        name=f"{author.name}#{author.discriminator}", icon_url=author.avatar_url)
    embed.set_footer(text=f"Deleted in : #{channel_name}")

    await ctx.channel.send(embed=embed)

client.run(token, bot=True)
1

1 Answers

1
votes

I'm going to suggest two slightly different solutions, because the code can be simpler if you're only running this bot on one guild. What's common to both is that you need to check in what channel messages are deleted, and in what channel the !snipe command is sent.

Single-Guild Version

If you're only monitoring/sniping one channel on one guild, then you can only ever have one deleted message to keep track of. Thus, you don't need a dictionary like in your posted code; you can just keep a single message or None.

You're already importing your token from a separate file, so you might as well put the channel ID (which is an int, unlike the bot token) there too for convenience. Note that, by convention, constants (variables you don't intend to change) are usually named in all caps in Python. tokens.py would look something like this:

TOKEN = 'string of characters here'
CHANNEL_ID = 123456789 # actually a 17- or 18-digit integer

And the bot itself:

import discord
from discord.ext import commands
from tokens import TOKEN, CHANNEL_ID

client = commands.Bot(command_prefix='!')
client.sniped_message = None

@client.event
async def on_ready():
    print("Your bot is ready.")

@client.event
async def on_message_delete(message):
    # Make sure it's in the watched channel, and not one of the bot's own
    # messages.
    if message.channel.id == CHANNEL_ID and message.author != client.user:
        print(f'sniped message: {message}')
        client.sniped_message = message

@client.command()
async def snipe(ctx):
    # Only respond to the command in the watched channel.
    if ctx.channel.id != CHANNEL_ID:
        return

    if client.sniped_message is None:
        await ctx.channel.send("Couldn't find a message to snipe!")
        return

    message = client.sniped_message

    embed = discord.Embed(
        description=message.content,
        color=discord.Color.purple(),
        timestamp=message.created_at
    )
    embed.set_author(
        name=f"{message.author.name}#{message.author.discriminator}",
        icon_url=message.author.avatar_url
    )
    embed.set_footer(text=f"Deleted in: #{message.channel.name}")

    await ctx.channel.send(embed=embed)

client.run(TOKEN)

Multi-Guild Version

If you're monitoring one channel each in multiple guilds, then you need to keep track of them separately. Handily, channel IDs are globally unique, not just within a single guild. So you can keep track of them by ID alone, without having to include the guild ID as well.

You could keep them in a list, but I recommend a set, because checking whether something is in a set or not is faster. Comments to help yourself remember which one is which are probably also a good idea.

TOKEN = 'string of characters here'
# Not a dictionary, even though it uses {}
CHANNEL_IDS = {
    # That one guild
    123456789,
    # The other guild
    987654322,
}

Then instead of checking against the single channel ID, you check if it's in the set of multiple IDs.

import discord
from discord.ext import commands
from tokens import TOKEN, CHANNEL_IDS

client = commands.Bot(command_prefix='!')
client.sniped_messages = {}

@client.event
async def on_ready():
    print("Your bot is ready.")

@client.event
async def on_message_delete(message):
    # Make sure it's in a watched channel, and not one of the bot's own
    # messages.
    if message.channel.id in CHANNEL_IDS and message.author != client.user:
        print(f'sniped message: {message}')
        client.sniped_messages[message.channel.id] = message

@client.command()
async def snipe(ctx):
    # Only respond to the command in a watched channel.
    if ctx.channel.id not in CHANNEL_IDS:
        return

    try:
        message = client.sniped_messages[ctx.channel.id]
    # See note below
    except KeyError:
        await ctx.channel.send("Couldn't find a message to snipe!")
        return

    embed = discord.Embed(
        description=message.content,
        color=discord.Color.purple(),
        timestamp=message.created_at
    )
    embed.set_author(
        name=f"{message.author.name}#{message.author.discriminator}",
        icon_url=message.author.avatar_url
    )
    embed.set_footer(text=f"Deleted in: #{message.channel.name}")

    await ctx.channel.send(embed=embed)

client.run(TOKEN)

Note: bare except clauses, like in your original code, are not generally a good idea. Here we only want to catch KeyError, which is what is raised if the requested key isn't in the dictionary.

You could, optionally, implement the same logic in a different way:

message = client.sniped_messages.get(ctx.channel.id)
if message is None:
    await ctx.channel.send("Couldn't find a message to snipe!")
    return

A dictionary's .get() method returns the corresponding item just like normal indexing. However, if there is no such key, instead of raising an exception, it returns a default value (which is None if you don't specify one in the call to get).

If you're using Python 3.8+, the first two lines could also be combined using an assignment expression (using the "walrus operator"), which assigns and checks all at once:

if (message := client.sniped_messages.get(ctx.channel.id)) is None:
    await ctx.channel.send("Couldn't find a message to snipe!")
    return

These alternative options are mentioned for completeness; all of these ways of doing it are perfectly fine.