How to build a (very badly written) Instagram Bot

Published on: 22nd Feb, 2021

Getting Started

Before we start getting our hands dirty, it’s always best to know that we have all the tools and knowledge required at hand. This saves us a lot of time from searching if anything is required, or finding out something worse, that the project cannot be done at all due to a missing piece.

Below are the tools that are required:


  • A Reddit Account to pull data from subreddits
  • An Instagram Account to post data
  • A Heroku Account to deploy your bot
  • A moderate amount of Javascript / NodeJS knowledge
  • VSCode
  • A working PC and some time 😊

  • Setting Up the Project

    Let’s create a directory called ig-bot. Open your terminal and in the directory and run 

    npm init
    Keep hitting Enter to answer Y to all the questions, this will create a package.json in your directory.


    Creating A Reddit Bot

    Now that we have our directory structure set up, let’s create our reddit bot that will act as our Reddit user and get us awesome posts from Reddit.

    Prerequisite
  • A personal reddit account
  • Steps to create a Reddit bot
  • Go to  https://www.reddit.com/prefs/apps/. You will have to log in if you aren’t already.
  • Click the button with the label Create Another App...
  • Give it a name
  • Paste  https://not-an-aardvark.github.io/reddit-oauth-helper/  in the redirect uri input box.
  • It should look as follows

    Once you hit ‘Create App’, a bot will be created and you will be able to see the client secret and client ID

    It should look as follows

    Head on over tohttps://not-an-aardvark.github.io/reddit-oauth-helper/  and paste the clientID and clientSecret into the respective input fields, like so.

    We check the ‘Permanent’ checkbox and select the ‘read’ Scope, as we would like to keep the access indefinitely and we just need to retrieve the posts from Reddit. Click on ‘Generate Tokens’.

    Once clicked, it should open a new tab redirecting you to Reddit and asking you to authorize the bot. Click on ‘Allow’. Congratulations, you have just created your Reddit Bot!


    Instagram Account

    Good job on creating the Reddit bot, now we move onto Instagram. To be honest, there’s not much to do with the Instagram bot. You just need to follow the basic steps to create a new Instagram profile, and have the username and password at hand as they will be required in the upcoming steps.


    The (badly written) Code Part

    Time to finally get our hands dirty, and very dirty at that. Keep in mind that when I had started writing this bot, I didn’t have any specific structure in mind. As a result, the project structure is kinda messy and the final goal was just to make the bot, sort of ‘work’. Without further ado:

    Installing Packages

    Let’s start by opening our ‘ig-bot’ directory in VSCode. You should just have the ‘package.json’ file currently. Now, we need to download the packages that are required for our ‘ig-bot’ to run.

    Let’s start by opening our ‘ig-bot’ directory in VSCode. You should just have the ‘package.json’ file currently. Now, we need to download the packages that are required for our ‘ig-bot’ to run.

    npm install dotenv fluent-ffmpeg instagram-private-api mongodb mongoose sharp snoowrap

    Breakdown of what we have installed:

    Package NamePackage Description
    dotenvA package to use the .env file type so we can hide our client secrets and passwords from people who want to steal them. It’s also generally a good practice to use them instead of hardcoding.
    fluent-ffmpegA wrapper of the ‘ffmpeg’ library which enables us to manipulate media files. We will be using this to merge audio and video files together.
    instagram-private-apiAn Instagram API client to interact with our Instagram account. Used to post pictures/videos to Instagram
    mongodbA package to interact with MongoDB to save the postIDs that we have uploaded
    mongodbA package to interact with MongoDB to save the postIDs that we have uploaded
    sharpA package to resize images. We need to resize images as instagram has strict restrictions on what aspect ratio it allows to be uploaded.
    snoowrapReddit API to interact with our ‘ig-bot’, mainly to read the posts and download them..

    Ok, Ok this is where the coding starts I Promise

     

    Create a folder called src, in this folder create a new file calledindex.js. This will be our entrypoint to the app, meaning it will be the file that will contain the driver code which handles the rest of the code.

    index.js
    const dotenv = require("dotenv");
    const sharp = require("sharp");
    const { findByPostID, insertPostId } = require("./db/db");
    const { MongoClient } = require("mongodb");
    const fs = require("fs");
    const path = require("path");
    const {
      downloadFile,
      getTopPostOfOddlySatisfying,
      doStuffWithDownloadedImage,
      initiateSnoo,
      doStuffWithDownloadedVideo,
    } = require("./utils/helpers");
    const { IgApiClient } = require("instagram-private-api");
    
    const cleanup = (files) => {
      for (const file of files) {
        fs.unlink(path.resolve(__dirname, file), (err) => {
          if (err) throw err;
          console.log("successfully deleted", file);
        });
      }
    };
    
    const run = async (postNumber) => {
      dotenv.config();
      const client = new MongoClient(process.env.dbURI);
      const r = initiateSnoo(process);
      const content = await getTopPostOfOddlySatisfying(
        r,
        postNumber,
        postNumber + 1
      );
      const doesPostExist = await findByPostID(client, content.postID);
      if (!doesPostExist) {
        const ig = new IgApiClient();
        ig.state.generateDevice(process.env.IG_USERNAME);
        await ig.account.login(process.env.IG_USERNAME, process.env.IG_PASSWORD);
        try {
          if (content.type === "video") {
            await downloadFile(content.videoLink, "videoToBeUploaded.mp4");
            await downloadFile(content.audioLink, "soundToBeUploaded.mp3");
            await downloadFile(content.thumbnail, "thumbnail.jpg");
            await doStuffWithDownloadedVideo(ig, content);
            cleanup([
              "../thumbnail.jpg",
              "../final.mp4",
              "../videoToBeUploaded.mp4",
              "../soundToBeUploaded.mp3",
            ]);
          } else {
            await downloadFile(content.imageLink, "downloaded.jpeg");
            await doStuffWithDownloadedImage(sharp, ig, content);
            cleanup(["../output.jpg", "../downloaded.jpeg"]);
          }
        } catch (error) {
          console.log("🚀 ~ file: index.js ~ line 45 ~ run ~ error", error);
          await insertPostId(client, content.postID, content.title);
          await client.close();
          return { error: true };
        }
        await insertPostId(client, content.postID, content.title);
        await client.close();
      }
    };
    
    let postNumber = new Date(Date.now()).getHours();
    run(postNumber);
    

    Create a folder called utils and create two new files in them helpers.js and captions.js

    helpers.js
    const fs = require("fs");
    const https = require("https");
    const { readFile } = require("fs");
    const { promisify } = require("util");
    const ffmpeg = require("fluent-ffmpeg");
    const readFileAsync = promisify(readFile);
    const snoowrap = require("snoowrap");
    const { imageTags, videoTags } = require("./captions");
    const path = require("path");
    const downloadFile = async (url, fileName) => {
      try {let file = fs.createWriteStream(`./${fileName}`);
        return new Promise((resolve, reject) => {
          https.get(url, (response) => {
            response.pipe(file);
            response.on("end", () => {
              resolve();
            });
          });
        });
      } catch (error) {
        console.log(
          "🚀 ~ file: helpers.js ~ line 30 ~ downloadFile ~ error",
          error
        );
      }
    };
    
    const getTopPostOfOddlySatisfying = async (r, postNumber, limit) => {
      try {
        const subreddit = await r.getSubreddit("oddlysatisfying");
        const topPost = await subreddit.getTop({ time: "day", limit })[postNumber];
        console.log(
          "🚀 ~ file: helpers.js ~ line 29 ~ getTopPostOfOddlySatisfying ~ topPost",
          JSON.stringify(topPost)
        );
    
        if (!topPost.is_video && topPost.domain === "i.redd.it") {
          return {
            type: "image",
            title: topPost.title,
            author: topPost.author.name,
            imageLink: topPost.url_overridden_by_dest,
            postID: topPost.id,
          };
        } else if (
          topPost.is_video &&
          !topPost.is_gif &&
          topPost.domain === "v.redd.it"
        ) {
          return {
            type: "video",
            title: topPost.title,
            author: topPost.author.name,
            postID: topPost.id,
            videoLink: topPost.media.reddit_video.fallback_url,
            thumbnail: topPost.thumbnail,
            audioLink: topPost.media.reddit_video.fallback_url.replace(
              /DASH_d+/,
              "DASH_audio"
            ),
          };
        }
      } catch (error) {
        console.log("ERROR", error);
      }
    };
    
    const mergeAudioAndVideo = async (audioFile, videoFile, resultFile) => {
      return new Promise((resolve, reject) => {
        try {
          let result = ffmpeg(videoFile)
            .addInput(audioFile)
            .size("720x?")
            .aspectRatio("4:5")
            .saveToFile(resultFile)
            .on("end", () => {
              resolve();
            });
        } catch (error) {
          console.log(
            "🚀 ~ file: helpers.js ~ line 70 ~ returnnewPromise ~ error",
            error
          );
    
          reject(error);
        }
      });
    };
    
    const doStuffWithDownloadedVideo = async (ig, content) => {
      await mergeAudioAndVideo(
        "videoToBeUploaded.mp4",
        "soundToBeUploaded.mp3",
        "final.mp4"
      );
      try {
        await ig.publish.video({
          // read the file into a Buffer
          coverImage: await readFileAsync(
            path.resolve(__dirname, "../../thumbnail.jpg")
          ),
          video: await readFileAsync(path.resolve(__dirname, "../../final.mp4")),caption: `${content.title} - Posted on r/oddlySatisfying by ${content.author} ${videoTags}`,
        });
    
        console.log("Done ig.publish.video ");
      } catch (error) {
        console.log(
          "🚀 ~ file: helpers.js ~ line 107 ~ doStuffWithDownloadedVideo ~ error",
          error
        );
        return {
          error: true,
          message: "Error occured",
        };
      }
    };
    
    const doStuffWithDownloadedImage = async (sharp, ig, content) => {
      await sharp("downloaded.jpeg")
        .resize({ width: 1080, height: 1080 })
        .toFormat("jpg")
        .toFile("output.jpg");
      await ig.publish.photo({
        // read the file into a Buffer
        file: await readFileAsync("output.jpg"),caption: `${content.title} - Posted on r/oddlySatisfying by ${content.author} ${imageTags},
      });
    };
    const initiateSnoo = (process) => {
      try {
        return new snoowrap({
          userAgent: "insta-uploader",
          clientId: process.env.clientId,
          clientSecret: process.env.clientSecret,
          accessToken: process.env.accessToken,
          refreshToken: process.env.refreshToken,
        });
      } catch (error) {
        console.log(
          "🚀 ~ file: helpers.js ~ line 97 ~ initiateSnoo ~ error",
          error
        );
      }
    };
    
    module.exports = {
      initiateSnoo,
      downloadFile,
      getTopPostOfOddlySatisfying,
      mergeAudioAndVideo,
      doStuffWithDownloadedImage,
      doStuffWithDownloadedVideo,
    };
    
    
    

    captions.js
    const imageTags = '#satisfaction #satisfying #satisfyingvideos #oddlysatisfying #satisfy  #satisfyingvideo #relax #reddit';
    
    const videoTags = '#satisfaction #satisfying #satisfyingvideos #oddlysatisfying #satisfy';
    
    module.exports = {
      imageTags,
      videoTags,
    };
    

    Create a folder called db and create a file in it called db.js. You can learn how to set up a DB here

    db.js
    const findByPostID = async (client, postId) => {
        try {
          await client.connect();
          const database = client.db("posts");
          const collection = database.collection("postID");
      
          const query = { postId };
      
          const post = await collection.findOne(query);
          console.log("🚀 ~ file: db.js ~ line 10 ~ findByPostID ~ post", post);
          return post;
        } catch (error) {
          console.log("🚀 ~ file: db.js ~ line 30 ~ insertPostId ~ error", error);
        }
      };
      
      const insertPostId = async (client, postId, postName) => {
        try {
          await client.connect();
          const database = client.db("posts");
          const collection = database.collection("postID");
      
          const doc = { postId, name: postName };
      
          const result = await collection.insertOne(doc); console.log(`${result.insertedCount} documents were inserted with the _id: ${result.insertedId}`);
        } catch (error) {
          console.log("🚀 ~ file: db.js ~ line 30 ~ insertPostId ~ error", error);
        }
      };
      
      module.exports = {
        findByPostID,
        insertPostId,
      };
      
    

    Create a .env file in the root directory (where you package.json is).

    .env
    
    IG_USERNAME=yourIGBotUsername
    IG_PASSWORD=yourIGBotPassword
    clientSecret=redditClientSecret
    accessToken=accessTokenProvidedBynot-an-aardvark
    refreshToken=refreshTokenProvidedBynot-an-aardvark
    clientId=redditClientId
    dbURI=mongoServerDBURI
    Finally, run 
    node src/index.js
      and watch your bot come to life. You could then deploy it to Heroku and add a free Scheduler Dyno to make a post every hour!