4
\$\begingroup\$

I created a twitter bot, that can parse a subreddit and post content from there to Twitter. I am running this bot using a cron-job, which starts the app using npm start after a 1-hour interval. Here is the code:

const Twit = require("twit");
const request = require("request").defaults({ encoding: null, });
const config = require("./config");

const fs = require("fs");
const path = require("path");

const T = new Twit(config);

main();

function getContent () {
    const options = {
        url: "https://www.reddit.com/r/freefolk/hot.json",
    };

    return new Promise(function (resolve, reject) {
        request.get(options, function (err, resp, body) {
            if (err) {
                reject(err);
            } else {
                resolve(parseSubReddit(JSON.parse(body)));
            }
        });
    });
}

function saveImage (url, path) {
    const options = {
        url: url,
    };
    return new Promise(function (resolve, reject) {
        request.get(options, function (err, resp, body) {
            if (err) {
                reject(err);
            } else {
                fs.writeFile(path, body, function (err) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve();
                    }
                });
            }
        });
    });
}

function parseSubReddit (response) {
    let posts = response.data.children;
    for (let post of posts.reverse()) {
        if (!post.data.is_self) {
            let data = {
                title: post.data.title,
                url: post.data.url,
            };
            return data;
        }
    }
}

function postTwit (content) {
    return new Promise(function (resolve, reject) {
        const localname = "downloaded_image";
        const PATH = path.join(__dirname, localname);
        let imagePromise = saveImage(content.url, PATH);
        imagePromise.then(function () {
            T.postMediaChunked({ file_path: PATH, }, function (err, data, response) {
                if (err) {
                    console.log("upload error");
                    reject(err);
                } else {
                    var mediaIdStr = data.media_id_string;
                    var altText = content.title;
                    var metaParams = { media_id: mediaIdStr, alt_text: { text: altText, }, };

                    T.post("media/metadata/create", metaParams)
                        .catch(function (err) {
                            console.log("error creating media meta data");
                            reject(err);
                        })
                        .then(function () {
                            // now we can reference the media and post a tweet (media will attach to the tweet)
                            var params = { status: content.title + " #gameofthrones",
                                media_ids: [ mediaIdStr, ], };

                            T.post("statuses/update", params)
                                .catch(function (err) {
                                    console.log("error in status update");
                                    reject(err);
                                })
                                .then(function (result) {
                                    fs.unlinkSync(PATH);
                                    resolve(result.data.text);
                                });
                        });
                }
            });
        }, function (err) {
            reject(err);
        });
    });
}

function main () {
    const contentPromise = getContent(postTwit);
    contentPromise.then(function (content) {
        // parse content to twit
        const twitted = postTwit(content);
        twitted.then(function (status) {
            console.log(status);
        }, function (err) {
            console.log("Something went wrong posting the twit: ", err);
        });
    }, function (err) {
        console.log("Error getting content: ", err);
    });
}

Link to GitHub Project

My questions/concerns are:

  1. Are the uses of promises correct? Can it be made better?
  2. I may have mixed callbacks and promises (e.g, while using fs). Is it ok to do so?
  3. Initially, I was using the top post from the subreddit to post on twitter. But I realized later, top posts remain at the top for more than an hour, creating repeated posts for my bot. So I am using the tenth (10 responses are returned by the Reddit API at a time) response as no post stays at position 10 for too long. I thought of choosing at random from first 10 but was sceptical of that method creating unique posts each time. And posting the last content was easy to implement also. Any other way to ensure uniqueness?
  4. How can things be generalized so that it can be extended to post different types of content?
  5. Any general suggestion about readability, functionality, style or cleanliness is most welcome.
\$\endgroup\$
2
\$\begingroup\$
  1. Nesting promises defeats the purpose of promises as a whole. Promises can be chained together. This is done by returning a promise to then.

    function postTwit() {
    
      return saveImage(content.url, PATH)
        .then(() => {
          // Return a promise to a then. The promise returned by then
          // will not resolve until this promise resolves.
          return T.postMediaChunkedPromiseVersion()
        })
        .then(data => {
          const metaParams = {...}
          return T.post("media/metadata/create", metaParams)
        })
        .then(() => {
          const params = {...};
    
          return T.post("statuses/update", params)
        })
        .then(result => {
          fs.unlinkSync(PATH);
          resolve(result.data.text);
        })
        .catch(e => {
          // Log error
        })
    }
    
    // Or even better with async-await:
    
    
    async function postTwit() {
      try {
        await saveImage(content.url, PATH)
    
        const data = await T.postMediaChunkedPromiseVersion()
    
        const metaParams = {...}
        await T.post("media/metadata/create", metaParams)
    
        const params = {...}
        const results = await T.post("statuses/update", params)
    
        fs.unlinkSync(PATH);
        return result.data.text
    
      } catch (e) {
        // Log the error
      }
    }
    

    If you have callback-style APIs that have no promise alternative, wrap them in promises so that you can still do chaining/async-await. Also, your error object should contain enough information so that regardless if you log in between operations or attach catch at the end, you can still extract enough information for the logging to make sense.

  2. The request module has a promise-based version. You can use that instead of manually wrapping the vanilla API with a promise. The fs module also has a promise-based version as well. You can also use async and await instead of promises for less nested code.

  3. Not sure how the Reddit API works. But if it works like the site, I suggest you use "New" content instead. Also, each item has an ID. Your app could keep a record of ones that have been Tweeted to avoid duplicates. This should work regardless of new or hot content, randomized or not.

  4. Inheritance or composition.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.