├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── app └── services │ └── twitter │ └── re_tweet_service.rb └── config └── application.yml.sample /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore application configuration 2 | config/application.yml 3 | 4 | # Ignore byebug history 5 | .byebug_history 6 | 7 | # Ignore aws instance pem file 8 | ruby-twitter-bot.pem 9 | 10 | # Ignore IDE folders 11 | .idea/ 12 | 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby '2.7.6' 5 | 6 | gem 'twitter', '~> 7.0.0' 7 | gem 'figaro', '~> 1.2.0' 8 | 9 | group :development, :test do 10 | gem 'pry-byebug', '~> 3.9.0' 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | buftok (0.2.0) 7 | byebug (11.1.3) 8 | coderay (1.1.3) 9 | domain_name (0.5.20190701) 10 | unf (>= 0.0.5, < 1.0.0) 11 | equalizer (0.0.11) 12 | ffi (1.15.5) 13 | ffi-compiler (1.0.1) 14 | ffi (>= 1.0.0) 15 | rake 16 | figaro (1.2.0) 17 | thor (>= 0.14.0, < 2) 18 | http (4.4.1) 19 | addressable (~> 2.3) 20 | http-cookie (~> 1.0) 21 | http-form_data (~> 2.2) 22 | http-parser (~> 1.2.0) 23 | http-cookie (1.0.5) 24 | domain_name (~> 0.5) 25 | http-form_data (2.3.0) 26 | http-parser (1.2.3) 27 | ffi-compiler (>= 1.0, < 2.0) 28 | http_parser.rb (0.6.0) 29 | memoizable (0.4.2) 30 | thread_safe (~> 0.3, >= 0.3.1) 31 | method_source (1.0.0) 32 | multipart-post (2.2.3) 33 | naught (1.1.0) 34 | pry (0.13.1) 35 | coderay (~> 1.1) 36 | method_source (~> 1.0) 37 | pry-byebug (3.9.0) 38 | byebug (~> 11.0) 39 | pry (~> 0.13.0) 40 | public_suffix (4.0.7) 41 | rake (13.0.6) 42 | simple_oauth (0.3.1) 43 | thor (1.2.1) 44 | thread_safe (0.3.6) 45 | twitter (7.0.0) 46 | addressable (~> 2.3) 47 | buftok (~> 0.2.0) 48 | equalizer (~> 0.0.11) 49 | http (~> 4.0) 50 | http-form_data (~> 2.0) 51 | http_parser.rb (~> 0.6.0) 52 | memoizable (~> 0.4.0) 53 | multipart-post (~> 2.0) 54 | naught (~> 1.0) 55 | simple_oauth (~> 0.3.0) 56 | unf (0.1.4) 57 | unf_ext 58 | unf_ext (0.0.8.2) 59 | 60 | PLATFORMS 61 | arm64-darwin-20 62 | ruby 63 | 64 | DEPENDENCIES 65 | figaro (~> 1.2.0) 66 | pry-byebug (~> 3.9.0) 67 | twitter (~> 7.0.0) 68 | 69 | RUBY VERSION 70 | ruby 2.7.6p219 71 | 72 | BUNDLED WITH 73 | 2.3.7 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Prabin Poudel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ruby-twitter-bot 2 | Twitter bot which retweets #rails and #ruby hashtags (case insensitive). Developed with Ruby. 3 | 4 | # Run the bot 5 | 6 | 1. Install dependencies 7 | 8 | `bundle install` 9 | 10 | 2. Copy `config/application.yml` from `config/application.yml.sample` and add all required values that you get from Twitter Api configs 11 | 12 | 3. Run the bot from project root 13 | 14 | `ruby app/services/twitter/re_tweet_service.rb` 15 | 16 | 4. You can change the hashtags you want the bot to retweet from inside `app/services/twitter/re_tweet_service.rb` 17 | 18 | - Update constant `HASHTAGS_TO_WATCH` 19 | 20 | 5. Run bot in background 21 | 22 | ``` 23 | # Create a new shell 24 | $ screen -S twitter-bot 25 | 26 | # Run the twitter bot (you should be inside project root) 27 | $ ruby app/services/twitter/re_tweet_service.rb 28 | 29 | # Detach ruby bot and move to original screen 30 | $ CTRL + a + d 31 | 32 | # Return to the screen where bot is running 33 | $ screen -r twitter-bot 34 | ``` 35 | 36 | Ref: [Run Ruby script in the background](https://stackoverflow.com/a/6391255/9359123) 37 | 38 | **NOTE:** If you have deployed bot to remote server, you need to restart the bot after server restart because it kills the script running in background 39 | 40 | # TODO 41 | 42 | 1. Allow to update the hashtags from `application.yml`. 43 | 2. Create initializer file and add gem require and figaro config to it. 44 | -------------------------------------------------------------------------------- /app/services/twitter/re_tweet_service.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'twitter' 5 | require 'figaro' 6 | require 'pry-byebug' 7 | 8 | Figaro.application = Figaro::Application.new( 9 | environment: 'production', 10 | path: File.expand_path('config/application.yml') 11 | ) 12 | 13 | Figaro.load 14 | 15 | module Twitter 16 | class ReTweetService 17 | def initialize 18 | @config = twitter_api_config 19 | @rest_client = configure_rest_client 20 | end 21 | 22 | def perform 23 | stream_client = configure_stream_client 24 | 25 | while true 26 | puts 'Starting to Retweet 3, 2, 1 ... NOW!' 27 | 28 | fetch_and_store_sensitive_users 29 | 30 | re_tweet(stream_client) 31 | end 32 | end 33 | 34 | private 35 | 36 | MAXIMUM_HASHTAG_COUNT = 3 37 | HASHTAGS_TO_WATCH = %w[#rails #ruby #RubyOnRails] 38 | 39 | attr_reader :config, :rest_client 40 | attr_accessor :sensitive_user_ids, :last_fetched_on 41 | 42 | def twitter_api_config 43 | { 44 | consumer_key: ENV['CONSUMER_KEY'], 45 | consumer_secret: ENV['CONSUMER_SECRET'], 46 | access_token: ENV['ACCESS_TOKEN'], 47 | access_token_secret: ENV['ACCESS_TOKEN_SECRET'] 48 | } 49 | end 50 | 51 | def configure_rest_client 52 | puts 'Configuring Rest Client' 53 | 54 | Twitter::REST::Client.new(config) 55 | end 56 | 57 | def configure_stream_client 58 | puts 'Configuring Stream Client' 59 | 60 | Twitter::Streaming::Client.new(config) 61 | end 62 | 63 | def fetch_and_store_sensitive_users 64 | blocked_user_ids = rest_client.blocked_ids.collect(&:to_i) 65 | muted_user_ids = rest_client.muted_ids.collect(&:to_i) 66 | 67 | @sensitive_user_ids = [blocked_user_ids, muted_user_ids].flatten.uniq 68 | @last_fetched_on = Time.now 69 | end 70 | 71 | def sensitive_users_fetch_time_expired? 72 | Time.now.hour != last_fetched_on.hour 73 | end 74 | 75 | def hashtags(tweet) 76 | tweet_hash = tweet.to_h 77 | extended_tweet = tweet_hash[:extended_tweet] 78 | 79 | (extended_tweet && extended_tweet[:entities][:hashtags]) || tweet_hash[:entities][:hashtags] 80 | end 81 | 82 | def tweet?(tweet) 83 | tweet.is_a?(Twitter::Tweet) 84 | end 85 | 86 | def retweet?(tweet) 87 | tweet.retweet? 88 | end 89 | 90 | def allowed_hashtags?(tweet) 91 | includes_allowed_hashtags = false 92 | 93 | hashtags(tweet).each do |hashtag| 94 | if HASHTAGS_TO_WATCH.map(&:upcase).include?("##{hashtag[:text]&.upcase}") 95 | includes_allowed_hashtags = true 96 | 97 | break 98 | end 99 | end 100 | 101 | includes_allowed_hashtags 102 | end 103 | 104 | def allowed_hashtag_count?(tweet) 105 | hashtags(tweet)&.count <= MAXIMUM_HASHTAG_COUNT 106 | end 107 | 108 | def sensitive_tweet?(tweet) 109 | tweet.possibly_sensitive? 110 | end 111 | 112 | def from_muted_or_blocked_user?(tweet) 113 | user_id = tweet.user.id 114 | 115 | sensitive_user_ids.include?(user_id) 116 | end 117 | 118 | def should_re_tweet?(tweet) 119 | tweet?(tweet) && !retweet?(tweet) && allowed_hashtag_count?(tweet) && !sensitive_tweet?(tweet) && allowed_hashtags?(tweet) && !from_muted_or_blocked_user?(tweet) 120 | end 121 | 122 | def re_tweet(stream_client) 123 | stream_client.filter(:track => HASHTAGS_TO_WATCH.join(',')) do |tweet| 124 | puts "\nCaught the tweet -> #{tweet.text}" 125 | 126 | fetch_and_store_sensitive_users if sensitive_users_fetch_time_expired? 127 | 128 | if should_re_tweet?(tweet) 129 | rest_client.retweet tweet 130 | 131 | puts "[#{Time.now}] Retweeted successfully!\n" 132 | end 133 | end 134 | rescue StandardError => e 135 | puts "=========Error========\n#{e.message}" 136 | 137 | puts "[#{Time.now}] Waiting for 60 seconds ....\n" 138 | 139 | sleep 60 140 | end 141 | end 142 | end 143 | 144 | Twitter::ReTweetService.new.perform 145 | -------------------------------------------------------------------------------- /config/application.yml.sample: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | CONSUMER_KEY: '' 3 | CONSUMER_SECRET: '' 4 | ACCESS_TOKEN: '' 5 | ACCESS_TOKEN_SECRET: '' 6 | 7 | development: 8 | <<: *defaults 9 | 10 | production: 11 | <<: *defaults 12 | --------------------------------------------------------------------------------