2
votes

I have a bot which responds to the !stock command.

When a user types !stock, the user message is deleted & an embed is sent to the channel with an image as an attachment.

This image is uniquely generated via Puppeteer (using an HTML string that I build based on some Mongo data I retrieve from a database).

Here is my code:

const config = require('../config');
const {
    doesGivenUserHaveGivenRole,
    generateStockImage
} = require('../utils');

module.exports = {
    name: 'stock',
    description: 'Use to post the latest stock levels',
    guildOnly: true,
    async execute(message, args) {
        // don't allow arguments
        if (args && args.length > 0) return;

        // ignore if we're not an admin
        if (!doesGivenUserHaveGivenRole(message, message.author.id, config.ADMIN_ROLE_ID)) return;

        // delete the author's message
        message.delete();

        // generate a new HTML string & return an embed to send to the channel
        const generatedStockImage = await generateStockImage();

        // send that message
        message.channel.send(generatedStockImage).then(message => {
        // then every minute
            setInterval(async () => {
        // get the image again
                const generatedEdit = await generateStockImage();
        // edit the original message with the new image (simple, right?)
                message.edit(generatedEdit);
            }, 60000);
        });
    }
};

The generateStockImage function looks like this:

const generateStockImage = async () => {

    // check if the image already exists
    if (fs.existsSync('./image.png')) {
        // if it does
        fs.unlink('./image.png', (err) => {
            if (err) {
                console.error(err)
                return;
            }

            // remove it, as we want to generate a new one for sure
            console.log('removed');
        });
    }

    // get my data from MongoDB
    const stock = await getStock();
    console.log(stock[0].stock);

    let html = `<!DOCTYPE html><html><head><style>body{background-color: black; color: white;width: 870px;height: 225px;}table{font-family: arial, sans-serif; border-collapse: collapse;}td, th{border: 1px solid #dddddd; padding: 4px; text-align: center;}.denomination{text-align: centre; font-size: 14px;margin: 0px 0px 10px 0px;}.amount{font-size: 35px;}.container{display: flex;}.table{margin: 0px 5px 0px 0px; width: 100%;}.green{color: #2ecc71;}.yellow{color: #f1c40f;}.red{color: #c45563;}.icon{width:20px;height:20px;padding-right:3px}</style></head><body><div class="container"> `;

    stock.forEach(code => {
        let colour;
        if (code.stock >= 100) {
            colour = 'green';
        }
        if (code.stock <= 50) {
            colour = 'yellow';
        }
        if (code.stock === 0) {
            colour = 'red';
        }

        html += `<!-- some html code I generate dynamically based on stock (above) ${code.something} -->`;
    });

    // close my html 
    html += `</div></body></html>`;

    // create a new puppeteer browser
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.setViewport({
        width: 880,
        height: 235,
        deviceScaleFactor: 1,
    });
    await page.setContent(html);
    await page.screenshot({ path: './image.png' }); // take a screenshot of the page we generated
    await browser.close();
    
    // wait for 8 seconds (to make sure the image is written to file first)
    await sleep(8000);

    const buffer = fs.readFileSync('./image.png');
    const attachment = new Discord.MessageAttachment(buffer, 'image.png');

    const embedToReturn = {
        embed: {
            color: '#4287f5',
            title: 'Current Stock',
            files: [attachment],
            image: {
                url: 'attachment://image.png'
            },
            timestamp: new Date(),
            footer: {
                text: 'My Bot Name',
                icon_url: 'https://i.imgur.com/myBotLogo.png',
            }
        }
    }

    console.log(embedToReturn);
    return embedToReturn; // return the new embed value
}

My code successfully generates a new image, based on the database data, and saves it to the project root.

I can open the file & see that it is in fact a newly generated image, based on the data I receive from MongoDB.

This image is then successfully posted the first time the command is ran, but does not update on subsequent 'edits'.

The issue I am facing is that the generated image does not get updated in my embed.

When the message.edit event gets triggered, it appears that it does not use generatedEdit. The image remains exactly the same, even though the embed has a new timestamp & the (edited) text on the message itself.

It successfully 'edits' the message every minute, but just doesn't show me the latest generated image (even though that image is in my project root, and I can see that it has been updated).

I have a feeling it's the following line that's causing the issues:
const attachment = new Discord.MessageAttachment(buffer, 'image.png');

Is this something to do with the discord cache? What am I doing wrong?

1
Had you tried console.loging the generatedEdit right before you edit the message to make sure that it is the right image that you are trying to send?Levi_OP
@Levi_OP Yep, and also checked the buffer to make sure the images are different, also checked the output filenopassport1
I'm thinking it's got to be something with the cache then. You could try renaming the image each time you attach it so that discord might not rely on the cache.Levi_OP
Is what you're saying that you're trying to edit the message with a different image?Itamar S
@Bqre yep. That’s right. After 60 seconds, create a new image, place it in the existing embednopassport1

1 Answers

3
votes

I couldn't find any logic errors just by seeing at your code, it should be working as expected.

There is a possibility though that as you mention, Discord is caching the image data, based on the same file name being provided.

Some suggestions:

Try giving an unique name to each image file you are sending to the Discord server.

const generateStockImage = async () => {
    const imageName = `image-${Date.now()}.png`
   
    // note: old cleanup part removed since now each image name is unique.
    // you could for example remove all images except the current "imageName" one 

    // get my data from MongoDB
    const stock = await getStock();
    console.log(stock[0].stock);

    let html = `<!DOCTYPE html><html><head><style>body{background-color: black; color: white;width: 870px;height: 225px;}table{font-family: arial, sans-serif; border-collapse: collapse;}td, th{border: 1px solid #dddddd; padding: 4px; text-align: center;}.denomination{text-align: centre; font-size: 14px;margin: 0px 0px 10px 0px;}.amount{font-size: 35px;}.container{display: flex;}.table{margin: 0px 5px 0px 0px; width: 100%;}.green{color: #2ecc71;}.yellow{color: #f1c40f;}.red{color: #c45563;}.icon{width:20px;height:20px;padding-right:3px}</style></head><body><div class="container"> `;

    stock.forEach(code => {
        let colour;
        if (code.stock >= 100) {
            colour = 'green';
        }
        if (code.stock <= 50) {
            colour = 'yellow';
        }
        if (code.stock === 0) {
            colour = 'red';
        }

        html += `<!-- some html code I generate dynamically based on stock (above) ${code.something} -->`;
    });

    // close my html 
    html += `</div></body></html>`;

    // create a new puppeteer browser
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.setViewport({
        width: 880,
        height: 235,
        deviceScaleFactor: 1,
    });
    await page.setContent(html);
    await page.screenshot({ path: `./${imageName}` }); // take a screenshot of the page we generated
    await browser.close();
    
    // wait for 8 seconds (to make sure the image is written to file first)
    await sleep(8000);

    const buffer = fs.readFileSync(`./${imageName}`);
    const attachment = new Discord.MessageAttachment(buffer, imageName);

    const embedToReturn = {
        embed: {
            color: '#4287f5',
            title: 'Current Stock',
            files: [attachment],
            image: {
                url: `attachment://${imageName}`
            },
            timestamp: new Date(),
            footer: {
                text: 'My Bot Name',
                icon_url: 'https://i.imgur.com/myBotLogo.png',
            }
        }
    }

    console.log(embedToReturn);
    return embedToReturn; // return the new embed value
}

Also something to consider, in this part:

        // send that message
        message.channel.send(generatedStockImage).then(message => {
        // then every minute
            setInterval(async () => {
        // get the image again
                const generatedEdit = await generateStockImage();
        // edit the original message with the new image (simple, right?)
                message.edit(generatedEdit);
            }, 60000);
        });

Notice that in the setInterval() you are referencing the message variable, but it's ambigous if your intent is referring to the outer context message one, or the inner context message variable in your callback.

To make your intent explicit and prevent possible bugs due to unexpected behavior, I´d suggest not shadowing the variable inside your callback. Tip: there is even an ESlint rule for that, worth of consideraton: no-shadow

        // send that message
        message.channel.send(generatedStockImage).then(sentMessage => {
        // then every minute
            setInterval(async () => {
        // get the image again
                const generatedEdit = await generateStockImage();
        // edit the original message with the new image (simple, right?)
                sentMessage.edit(generatedEdit);
            }, 60000);
        });

If referencing the sentVariable I've just renamed would not work for you, then you can directly omit it and just keep the original reference:

        // send that message
        message.channel.send(generatedStockImage).then(() => { // or, if want to still see it around, append with _: (_message) =>
        // then every minute
            setInterval(async () => {
        // get the image again
                const generatedEdit = await generateStockImage();
        // edit the original message with the new image (simple, right?)
                message.edit(generatedEdit);
            }, 60000);
        });