├── Procfile ├── index.js ├── index.spec.js ├── package.json ├── README.md ├── .circleci └── config.yml ├── .gitignore ├── twitter-bot.spec.js └── twitter-bot.js /Procfile: -------------------------------------------------------------------------------- 1 | worker: node index.js -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const TwitterBot = require('./twitter-bot.js'); 2 | 3 | // Initiate Bot 4 | function BotInit() { 5 | TwitterBot.BotRetweet(); 6 | } 7 | 8 | BotInit(); 9 | 10 | module.exports = { 11 | BotInit, 12 | }; 13 | -------------------------------------------------------------------------------- /index.spec.js: -------------------------------------------------------------------------------- 1 | const Index = require('./index.js') 2 | 3 | describe('Index', () => { 4 | const BotInitMock = jest.fn(Index.BotInit) 5 | it('should BotInit to be defined', () => { 6 | expect(BotInitMock).toBeDefined(); 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "100diasdecodigo", 3 | "version": "1.0.1", 4 | "description": "A simple bot to retweet #100diasdecódigo", 5 | "repository": "https://github.com/jcserracampos/100diasdecodigo_bot", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index.js", 9 | "test": "jest" 10 | }, 11 | "author": "Júlio Campos ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "dotenv": "^6.0.0", 15 | "twit": "^2.2.10" 16 | }, 17 | "devDependencies": { 18 | "jest": "^26.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 100diasdecodigo_bot 2 | 3 | [![CircleCI](https://circleci.com/gh/jcserracampos/100diasdecodigo_bot/tree/master.svg?style=svg)](https://circleci.com/gh/jcserracampos/100diasdecodigo_bot/tree/master) 4 | 5 | ## Como utilizar 6 | Primeiramente, crie uma nova aplicação em https://apps.twitter.com/. 7 | Depois de criada a aplicação, solicite um novo __access token__ para sua conta. 8 | 9 | Com essas informações, crie um arquivo `.env` na raiz do projeto com a seguinte formatação. 10 | 11 | CONSUMER_KEY= 12 | CONSUMER_SECRET= 13 | ACCESS_TOKEN= 14 | ACCESS_TOKEN_SECRET= 15 | 16 | As chaves inseridas devem ser as obtidas no passo anterior. 17 | 18 | Instale as dependências com `npm install`. 19 | 20 | Finalmente, execute `npm start`. 21 | 22 | ## Deploy no heroku 23 | Existe um Procfile configurado para este bot funcionar como um worker no heroku. 24 | A versão gratuita atende bem, dependendo do volume de webhooks recebidos. 25 | 26 | As chaves devem ser inseridas como variáveis de configuração no Heroku. 27 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:latest 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # vuepress build output 64 | .vuepress/dist 65 | 66 | # Serverless directories 67 | .serverless 68 | .vscode/launch.json 69 | .DS_Store 70 | -------------------------------------------------------------------------------- /twitter-bot.spec.js: -------------------------------------------------------------------------------- 1 | const TwitterBot = require('./twitter-bot.js'); 2 | const Twit = require('twit'); 3 | 4 | // Smoke Tests 5 | describe('Smoke Tests', () => { 6 | describe('Bot', () => { 7 | it('should exists as object', () => { 8 | expect(typeof TwitterBot.Bot).toBe('object'); 9 | }); 10 | 11 | it('should be instance of Twit', () => { 12 | expect(TwitterBot.Bot).toBeInstanceOf(Twit); 13 | }); 14 | 15 | it('should Bot config have twitter access properties', () => { 16 | const expected = { 17 | consumer_key: expect.anything(), 18 | consumer_secret: expect.anything(), 19 | access_token: expect.anything(), 20 | access_token_secret: expect.anything(), 21 | }; 22 | expect(TwitterBot.Bot.config).toEqual(expect.objectContaining(expected)) 23 | }); 24 | 25 | }); 26 | 27 | describe('BotRetweet', () => { 28 | it('should BotRetweet exist and be a function', () => { 29 | expect(typeof TwitterBot.BotRetweet).toBe('function'); 30 | }); 31 | }); 32 | 33 | describe('isReply', () => { 34 | it('should exist and be a function', () => { 35 | expect(typeof TwitterBot.isReply).toBe('function'); 36 | }); 37 | }); 38 | 39 | }) 40 | 41 | -------------------------------------------------------------------------------- /twitter-bot.js: -------------------------------------------------------------------------------- 1 | const Twit = require('twit'); 2 | 3 | require('dotenv').config(); 4 | 5 | /* Configure the Twitter API */ 6 | const Bot = new Twit({ 7 | consumer_key: process.env.CONSUMER_KEY, 8 | consumer_secret: process.env.CONSUMER_SECRET, 9 | access_token: process.env.ACCESS_TOKEN, 10 | access_token_secret: process.env.ACCESS_TOKEN_SECRET, 11 | timeout_ms: 60 * 1000, 12 | }); 13 | 14 | var TWITTER_SEARCH_PHRASE = '#100diasdecodigo'; 15 | 16 | console.log('The bot is running...'); 17 | 18 | /* BotRetweet() : To retweet recent tweets with our query */ 19 | function BotRetweet() { 20 | const stream = Bot.stream('statuses/filter', { 21 | track: TWITTER_SEARCH_PHRASE, 22 | language: 'pt' 23 | }); 24 | 25 | stream.on('tweet', tweet => { 26 | if(isReply(tweet)) { 27 | console.warn('Tweet is a retweet!'); 28 | } else { 29 | Bot.post('statuses/retweet/:id', { 30 | id: tweet.id_str 31 | }, (error, response) => { 32 | if (error) { 33 | console.log('Bot could not retweet, : ' + error); 34 | } else { 35 | console.log('Bot retweeted : ' + response.text); 36 | } 37 | }); 38 | } 39 | }); 40 | }; 41 | 42 | function isReply(tweet) { 43 | if ( tweet.retweeted_status 44 | || tweet.in_reply_to_status_id 45 | || tweet.in_reply_to_status_id_str 46 | || tweet.in_reply_to_user_id 47 | || tweet.in_reply_to_user_id_str 48 | || tweet.in_reply_to_screen_name ) 49 | return true 50 | } 51 | 52 | // Exports 53 | module.exports = { 54 | Bot, 55 | BotRetweet, 56 | isReply, 57 | } --------------------------------------------------------------------------------