A Twitter Bot to retweet any tweets with certain keywords or hashtags built with Node.js, TypeScript, DI and IoC.
In this article, we are going to build a Twitter Bot which would retweet any public tweets including one or more keywords or hashtags.
However, please keep in mind the following:
The Bot we are going to create is just a sample Bot. It is created for educational purposes and could not be used as a fully-fledged production material.
You can use what you would learn from this article, and the shared code, as a starter kit for you to start working on your production code if you wish.
You might question how complicated the solution is for that simple task. I totally agree with you that the same output could have been achieved using a simpler implementation. However, you can think about it as a training and an opportunity to learn new things if you don’t already know them.
Now having that said, buckle up and enjoy the ride.
Design Details
First, let me explain some important design details to keep in mind while working on this project.
Patterns
Dependency Inversion (DI) and Inversion Of Control (IoC) Containers:
DI principle is applied to all project modules
InversifyJs node package is used as the IoC container
Languages and Frameworks
The project is built with Node.js and TypeScript.
Package Manager
npm package manager.
Node Packages
typeScript: for TypeScript support.
ts-node: TypeScript execution and REPL for node.js, with source map and native ESM support.
inversify: A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript.
config: Organizes hierarchical configurations for your app deployments.
twit: Twitter API Client for node.
@types/node: Contains type definitions for Node.js.
@types/config: Contains type definitions for node-config.
@types/twit: Contains type definitions for twit.
Modules and Services
ConfigService: This is the service responsible for providing the configurations for the whole system.
TwitterService: This is the service responsible for the integration with Twitter APIs. It wraps Twitter APIs -the ones we need in our project- and present them into an appropriate domain specific language (DSL).
TwitterBotService: This is the service responsible for wrapping the main Bot module.
DependencyInjectionService: This is the service responsible for creating the IoC container and setting the right types bindings.
Dependencies
ConfigService uses the config node package.
TwitterService uses the twit node package and depends on the ConfigService to read the Twitter API keys and tokens from the configurations.
TwitterBotService depends on the TwitterService to perform the proper Twitter API calls.
DependencyInjectionService doesn’t depend on other services but it is aware of each service interface/abstraction and its corresponding implementation.
Solution Structure
config: Directory where different environment configuration files exist. The accepted file names are provided by config node package as follows; default.json, development.json, production.json, and custom-environment-variables.json.
dist: Directory where all the TypeScript files are transpiled to be used later for deployment.
node_modules: Directory where all node modules reside.
src: Directory with all the code.
src/app: Sub-directory with all the code except the main index.ts file.
src/app/config: Directory with the Config Service code.
src/app/dependency-injection: Directory with the Dependency Injection Service code.
src/app/twitter: Directory with the Twitter Service code.
src/app/twitter-bot: Directory with the Twitter Bot Service code.
Prerequisites
Knowledge
TypeScript
Node.js
Git
Heroku
Accounts
Creating a Twitter account for your Bot.
Creating a Twitter application and getting API keys and tokens from Twitter Developer Dashboard.
Creating a Heroku account and application.
Software
Implementation
This is where the action begins, but first, please make sure the software prerequisites are already installed before moving forward.
Creating the Project Solution
Create the main TwitterBot directory for the solution.
Open the command line and browse to the main directory created on the first step.
Run npm init and set your project settings or npm init --yes for defaults.
Run npm install config inversify twit ts-node typescript
Run npm install @types/config @types/node @types/twit
Run tsc init
Run code . to open VS Code.
Create the src folder under the root directory.
Create an empty index.ts under the src directory.
Create the app folder under the src directory.
tsconfig.json
exclude => [“./node_modules”]
compilerOptions.target => “es6”
compilerOptions.lib => [“es6”, “dom”]
compilerOptions.experimentalDecorators => true
compilerOptions.emitDecoratorMetadata => true
compilerOptions.module => “commonjs”
compilerOptions.rootDir => “./src”
compilerOptions.moduleResolution => “node”
compilerOptions.types => [“reflect-metadata”]
compilerOptions.outDir => “./dist”
compilerOptions.esModuleInterop => true
compilerOptions.forceConsistentCasingInFileNames =>true
compilerOptions.strict => true
compilerOptions.skipLibCheck => true
package.json
main => “dist/index.js”
scripts => define “tsc”: “tsc”
scripts => define “test”: “echo \”Error: no test specified\” && exit 1"
scripts => define “build”: “rm -rf ./dist/ && npm run tsc — project”
scripts => define “start”: “ts-node ./src/index.ts”
scripts => define “postinstall”: “npm run tsc”
scripts => define “watch”: “tsc -p tsconfig.json -w”
On your command-line, run node --version and replace the “16.13.0” on the line below with your version.
engines => define “node”: “16.13.0”
On your command-line, run npm --version and replace the “8.1.2” on the line below with your version.
engines => define “npm”: “8.1.2”
Different Environments Configurations
First, follow these steps to create the folder/file structure and then I will explain what is going on.
Create the config folder under the root directory.
Create an empty default.json under config.
Modify the content of default.json to be {}.
Create an empty development.json under config.
Modify the content of development.json to be {}.
Create an empty production.json under config.
Modify the content of production.json to be {}.
Create an empty custom-environment-variables.json under config.
Modify the content of default.json to be as follows
Let me explain what is going on here:
The folder/file structure we created is consistent with the requirements of the config node package.
The config package checks for the process.env.NODE_ENV variable and loads the corresponding .json file.
The special thing here is the custom-environment-variables.json file. This file is used to keep the sensitive configurations hidden from the source code.
So, when we add “consumerKey”: “TWIT_CONSUMER_KEY” to the custom-environment-variables.json file, this means that whenever the configuration “consumerKey” is being retrieved, its value should be read from the process.env.TWIT_CONSUMER_KEY variable.
Later, we would know how to set these process.env.* variables on the deployment server where it should be safe.
To know more about config node package and its related setup, you can check this link.
Code Implementation
Now, we are ready for starting coding. I would split the code implementation into parts and for each part, I would explain the code and provide any helpful notes I have.
Config Service
This is the service responsible for providing the configurations for the whole system.
Steps:
Create config folder under src/app/.
Create config-service.ts under src/app/config.
Copy the code below to config-service.ts.
Explanation:
Here we are defining the interface IConfigManager which would provide the required configuration settings.
We are also defining the class ConfigManager which implements the interface IConfigManager.
In the properties implementations, we are using the config node package to read the configurations.
That’s why we needed import config from ‘config’;
We also decorated the class with the @injectable() decorator -defined by InverisfyJS node package- so that the ConfigManager class could be injected whenever needed.
However, this is not enough for InverisfyJS node package to do its job. We also need to define a Symbol to be used as an identification token for this injectable.
That’s why we added export const ConfigManagerLocator = { ConfigManager: Symbol.for(‘IConfigManager’) };
And we needed import “reflect-metadata”; import { injectable } from ‘inversify’;
Twitter Service
This is the service responsible for the integration with Twitter APIs. It wraps Twitter APIs -the ones we need in our project- and present them into an appropriate domain specific language (DSL).
Steps:
Create twitter folder under src/app/.
Create twitter-service.ts under src/app/twitter.
Copy the code below to twitter-service.ts.
Explanation:
Here we are defining the interface ITwitterService which would provide the WatchToFilterStream function.
We are also defining the class TwitterService which implements the interface ITwitterService.
Now, this class depends on the ConfigService to use it to get the configuration settings related to Twitter API keys and tokens.
That’s why we are injecting it in the constructor as follows @inject(ConfigManagerLocator.ConfigManager) private configManager: IConfigManager.
This is the way to instruct InversifyJS to inject an instance of IConfigManager.
Then, in the constructor implementation, we are using the injected IConfigManager to retrieve Twitter API keys and tokens.
We are also using the twit node package to perform Twitter APIs calls but first we need to create an object of it passing in the Twitter API keys and tokens we got from the injected IConfigManager.
For the WatchToFilterStream function, we are using twit node package to listen to the public tweets feed, filter them by the keywords passed in, and in case some interesting tweet is found, it would be re-tweeted.
The TwitterService class would be injectable as well. So, we are decorating it with the @injectable() decorator.
Also exported the symbol export const TwitterServiceLocator = { TwitterService: Symbol.for(‘ITwitterService’) };
And imported:
import "reflect-metadata"; import { inject, injectable } from 'inversify'; import { ConfigManagerLocator, IConfigManager } from './../config/config-service'; import Twit from 'twit';
Twitter Bot Service
This is the service responsible for wrapping the main Bot module.
Steps:
Create twitter-bot folder under src/app/.
Create twitter-bot.ts under src/app/twitter-bot.
Copy the code below to twitter-bot.ts.
Explanation:
Here we are defining the interface ITwitterBot which would provide the Initialize function.
We are also defining the class TwitterBot which implements the interface ITwitterBot.
Now, this class depends on the TwitterService.
That’s why we are injecting it in the constructor as follows @inject(TwitterServiceLocator.TwitterService) private twitterService: ITwitterService.
The Initialize function is just calling the WatchToFilterStream function passing in the array of keywords to be used to filter the public tweets.
The TwitterBot class would be injectable as well. So, we are decorating it with the @injectable() decorator.
Also exported the symbol export const TwitterBotLocator = { TwitterBot: Symbol.for(‘ITwitterBot’) };.
And imported import “reflect-metadata”; import { TwitterServiceLocator } from ‘../twitter/twitter-service’; import { inject, injectable } from “inversify”; import { ITwitterService } from “../twitter/twitter-service”;.
Dependency Injection Service
This is the service responsible for creating the IoC container and setting the right types of bindings.
Steps:
Create dependency-injection folder under src/app/.
Create dependency-injection.ts under src/app/dependency-injection.
Copy the code below to dependency-injection.ts.
Explanation:
Here we are creating the IoC container using const container = new Container();.
Then we are binding each interface/abstraction to its default implementation.
container.bind<IConfigManager>(ConfigManagerLocator.ConfigManager).to(ConfigManager).inSingletonScope();.
container.bind<ITwitterService>(TwitterServiceLocator.TwitterService).to(TwitterService).inSingletonScope();.
container.bind<TwitterBot>(TwitterBotLocator.TwitterBot).to(TwitterBot).inSingletonScope();.
Don’t forget to import import “reflect-metadata”;.
And all the other required imports.
And finally, export default container;.
index.ts
This is where our project starts running.
Explanation:
We are using the container to create an instance of ITwitterBot.
const twitterBot = container.get<ITwitterBot>(TwitterBotLocator.TwitterBot);.
Then we are using this instance to call the Initialize function passing in the keywords javascript, dotnet, and nodejs.
Don’t forget to import import “reflect-metadata”;.
And all the other required imports.
Pushing to Git
Explaining how to use GIT is out of scope of this article. However, one of the important things to keep in mind is the .gitignore file.
I am using the default “node” template from GIT and just added the final line .env.
Deployment to Heroku
Explaining how to deploy to Heroku is out of scope of this article. However, one of the important things to keep in mind is how to set the process.env variables on Heroku. This could be achieved by two ways.
Through Command-line:
Using the command line, run heroku login.
Follow the on-screen instructions to log in to Heroku successfully.
Run the following but don’t forget to replace the your-something part with the keys and tokens you got from Twitter.
heroku config:set NPM_CONFIG_PRODUCTION=true.heroku config:set NODE_ENV="production".heroku config:set TWIT_CONSUMER_KEY="your-consumer-key".heroku config:set TWIT_CONSUMER_SECRET="your-consumer-secret".heroku config:set TWIT_ACCESS_TOKEN="your-access-token".heroku config:set TWIT_ACCESS_TOKEN_SECRET="your-access-token-secret".
Through Heroku Dashboard
See it In Action
Now, if you go to the Twitter account you created for the Bot, you should see some retweets. It could take some time depending on the keywords, but it should happen at last.
How to Pause The Bot
Browse to your Heroku account, your app, Resources tab, hit the pencil to edit, and finally turn the radio button off as in the image below.
That’s it, hope you found reading this article as interesting as I found writing it.
Comments