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:
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
Steps to create a Reddit bot
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 Name | Package Description |
---|---|
dotenv | A 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-ffmpeg | A 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-api | An Instagram API client to interact with our Instagram account. Used to post pictures/videos to Instagram |
mongodb | A package to interact with MongoDB to save the postIDs that we have uploaded |
mongodb | A package to interact with MongoDB to save the postIDs that we have uploaded |
sharp | A package to resize images. We need to resize images as instagram has strict restrictions on what aspect ratio it allows to be uploaded. |
snoowrap | Reddit 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
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!