class Twittbot::Bot

Class providing the streaming connections and callback logic for the bot.

Public Class Methods

new(options = {}) click to toggle source
# File lib/twittbot/bot.rb, line 15
def initialize(options = {})
  @options = {
      current_dir: FileUtils.pwd
  }

  $bot = {
      botparts: [],
      callbacks: {},
      commands: {},
      config: Twittbot::DEFAULT_BOT_CONFIG.merge(
          YAML.load_file(File.expand_path("./#{Twittbot::CONFIG_FILE_NAME}", @options[:current_dir]))
      ),
      periodic: [],
      save_config: true,
      tasks: {},
      stream: options.fetch("stream", true)
  }

  load_bot_code

  at_exit do
    save_config
    $bot[:botparts].each { |b| b.save_config }
  end if $bot[:save_config]
end

Public Instance Methods

already_authed?() click to toggle source

@return [Boolean] whether the bot is already authenticated or not.

# File lib/twittbot/bot.rb, line 270
def already_authed?
  !($bot[:config][:access_token].empty? or $bot[:config][:access_token_secret].empty?)
end
auth() click to toggle source

Authenticates an account with Twitter.

# File lib/twittbot/bot.rb, line 42
def auth
  require 'oauth'
  say "This will reset your current access tokens.", :red if already_authed?

  # get the request token URL
  callback = OAuth::OUT_OF_BAND
  consumer = OAuth::Consumer.new $bot[:config][:consumer_key],
                                 $bot[:config][:consumer_secret],
                                 site: Twitter::REST::Client::BASE_URL,
                                 scheme: :header
  request_token = consumer.get_request_token(oauth_callback: callback)
  url = request_token.authorize_url(oauth_callback: callback)

  puts "Open this URL in a browser: #{url}"
  pin = ''
  until pin =~ /^\d+$/
    print "Enter PIN =>"
    pin = $stdin.gets.strip
  end

  access_token = request_token.get_access_token(oauth_verifier: pin)
  $bot[:config][:access_token] = access_token.token
  $bot[:config][:access_token_secret] = access_token.secret

  # get the bot's user name (screen_name) and print it to the console
  $bot[:config][:screen_name] = get_screen_name access_token
  puts "Hello, #{$bot[:config][:screen_name]}!"
end
check_config() click to toggle source

Checks some configuration values, e.g. if the bot is already authenticated with Twitter

# File lib/twittbot/bot.rb, line 180
def check_config
  unless already_authed?
    say "Please authenticate using `twittbot auth' first.", :red
    raise 'Not authenticated'
  end
end
cron(task_name) click to toggle source

Runs the task `task_name`. @param task_name [Symbol] The name of the task to be run.

# File lib/twittbot/bot.rb, line 131
def cron(task_name)
  task = $bot[:tasks][task_name]
  unless task
    say "Task \"#{task_name}\" does not exist.  Use \"twittbot cron list\" for a list of available tasks."
    return
  end

  #check_config
  init_clients
  task[:block].call
end
do_callbacks(callback_type, object, options = {}) click to toggle source

Runs callbacks. @param callback_type [:Symbol] The callback type. @param object [Object] The object

# File lib/twittbot/bot.rb, line 230
def do_callbacks(callback_type, object, options = {})
  return if $bot[:callbacks][callback_type].nil?
  $bot[:callbacks][callback_type].each do |c|
    c[:block].call object, options
  end
end
do_direct_message(dm, opts = {}) click to toggle source

Processes a direct message. @param dm [Twitter::DirectMessage] received direct message

# File lib/twittbot/bot.rb, line 251
def do_direct_message(dm, opts = {})
  return if dm.sender.screen_name == $bot[:config][:screen_name]
  return do_callbacks(:direct_message, dm, opts) unless dm.text.start_with? $bot[:config][:dm_command_prefix]
  dm_text = dm.text.sub($bot[:config][:dm_command_prefix], '').strip
  return if dm_text.empty?

  return unless /(?<command>[A-Za-z0-9]+)(?:\s*)(?<args>.*)/m =~ dm_text
  command = Regexp.last_match(:command).to_sym
  args = Regexp.last_match :args

  cmd = $bot[:commands][command]
  return say_status :dm, "#{dm.sender.screen_name} tried to issue non-existent command :#{command}, ignoring", :cyan if cmd.nil?
  return say_status :dm, "#{dm.sender.screen_name} tried to issue admin command :#{command}, ignoring", :cyan if cmd[:admin] and !dm.sender.admin?

  say_status :dm, "#{dm.sender.screen_name} issued command :#{command}", :cyan
  cmd[:block].call(args, dm.sender)
end
do_periodic() click to toggle source
# File lib/twittbot/bot.rb, line 237
def do_periodic
  $bot[:periodic].each_with_index do |h, i|
    h[:remaining] = if h[:remaining] - 1 <= 0
                      h[:block].call
                      h[:interval]
                    else
                      h[:remaining] - 1
                    end
    $bot[:periodic][i] = h
  end
end
handle_stream_object(object, type) click to toggle source

Handles a object yielded from a Twitter::Streaming::Client. @param object [Object] The object yielded from a Twitter::Streaming::Client connection. @param type [Symbol] The type of the streamer. Should be either :user or :filter.

# File lib/twittbot/bot.rb, line 190
def handle_stream_object(object, type)
  opts = {
      stream_type: type
  }
  case object
    when Twitter::Streaming::FriendList
      # object: Array with IDs
      do_callbacks :friend_list, object
    when Twitter::Tweet
      # object: Twitter::Tweet
      is_mention = (object.user.screen_name != $bot[:config][:screen_name] and object.text.include?("@" + $bot[:config][:screen_name]) and not object.retweet?)
      do_callbacks :retweet, object, opts if object.retweet? and object.retweeted_tweet.user.screen_name == $bot[:config][:screen_name]
      do_callbacks :mention, object, opts if is_mention
      do_callbacks :tweet, object, opts.merge({ mention: is_mention, retweet: object.retweet?, favorite: object.favorited? })
    when Twitter::Streaming::Event
      case object.name
        when :follow, :favorite
          # :follow   -- object: Twitter::Streaming::Event(name: :follow,   source: Twitter::User, target: Twitter::User)
          # :favorite -- object: Twitter::Streaming::Event(name: :favorite, source: Twitter::User, target: Twitter::User, target_object: Twitter::Tweet)
          do_callbacks object.name, object, opts
        else
          puts "no handler for #{object.class.to_s}/#{object.name}\n  -- object data:"
          require 'pp'
          pp object
          do_callbacks object.name, object, opts
      end
    when Twitter::DirectMessage
      do_direct_message object, opts
    when Twitter::Streaming::DeletedTweet
      do_callbacks :deleted, object, opts
    else
      puts "no handler for #{object.class.to_s}\n  -- object data:"
      require 'pp'
      pp object
  end
end
load_bot_code() click to toggle source

Loads the bot's actual code which is stored in the bot's lib subdirectory.

# File lib/twittbot/bot.rb, line 162
def load_bot_code
  load_special_tasks
  files = Dir["#{File.expand_path('./lib', @options[:current_dir])}/**/*"]
  files.each do |file|
    require_relative file.sub(/\.rb$/, '') if file.end_with? '.rb'
  end
end
modify_admin(screen_name, action = :add) click to toggle source

@param screen_name [String] the user's screen name @param action [Symbol] :add or :delete

# File lib/twittbot/bot.rb, line 145
def modify_admin(screen_name, action = :add)
  init_clients
  user = $bot[:client].user screen_name
  case action
    when :add
      $bot[:config][:admins] << user.id unless $bot[:config][:admins].include? user.id
    when :del, :delete
      $bot[:config][:admins].delete user.id if $bot[:config][:admins].include? user.id
    else
      say "Unknown action " + action.to_s, :red
  end
rescue Twitter::Error::NotFound
  say "User not found.", :red
end
save_config() click to toggle source

Saves the bot's config (i.e. not the botpart ones).

# File lib/twittbot/bot.rb, line 171
def save_config
  config = $bot[:config].clone
  config.delete :client
  File.open "./#{Twittbot::CONFIG_FILE_NAME}", 'w' do |f|
    f.write config.to_yaml
  end
end
start() click to toggle source

Starts the bot.

# File lib/twittbot/bot.rb, line 72
def start
  check_config
  init_clients

  if $bot[:stream]
    puts "connecting to streaming APIs"

    @userstream_thread ||= Thread.new do
      loop do
        begin
          puts "connected to user stream"
          @streamer.user do |obj|
            handle_stream_object obj, :user
          end
        rescue => e
          puts "lost user stream connection: " + e.message
        end
        puts "reconnecting in #{Twittbot::RECONNECT_WAIT_TIME} seconds..."
        sleep Twittbot::RECONNECT_WAIT_TIME
      end
    end

    @tweetstream_thread ||= Thread.new do
      loop do
        begin
          puts "connected to tweet stream"
          @streamer.filter track: $bot[:config][:track].join(",") do |obj|
            handle_stream_object obj, :filter
          end
        rescue
          puts "lost tweet stream connection: " + e.message
        end
        puts "reconnecting in #{Twittbot::RECONNECT_WAIT_TIME} seconds..."
        sleep Twittbot::RECONNECT_WAIT_TIME
      end
    end
  end

  @periodic_thread ||= Thread.new do
    loop do
      begin
        Thread.new { do_periodic }
      rescue => _
      end
      sleep 60
    end
  end

  do_callbacks :load, nil

  if $bot[:stream]
    @userstream_thread.join
    @tweetstream_thread.join
  end
  @periodic_thread.join
end

Private Instance Methods

get_screen_name(access_token) click to toggle source
# File lib/twittbot/bot.rb, line 292
def get_screen_name(access_token)
  oauth_response = access_token.get('/1.1/account/verify_credentials.json?skip_status=true')
  oauth_response.body.match(/"screen_name"\s*:\s*"(.*?)"/).captures.first
end
init_clients() click to toggle source
# File lib/twittbot/bot.rb, line 276
def init_clients
  $bot[:client] ||= Twitter::REST::Client.new do |cfg|
    cfg.consumer_key        = $bot[:config][:consumer_key]
    cfg.consumer_secret     = $bot[:config][:consumer_secret]
    cfg.access_token        = $bot[:config][:access_token]
    cfg.access_token_secret = $bot[:config][:access_token_secret]
  end

  @streamer ||= Twitter::Streaming::Client.new do |cfg|
    cfg.consumer_key        = $bot[:config][:consumer_key]
    cfg.consumer_secret     = $bot[:config][:consumer_secret]
    cfg.access_token        = $bot[:config][:access_token]
    cfg.access_token_secret = $bot[:config][:access_token_secret]
  end
end
list_tasks() click to toggle source

Lists all tasks

# File lib/twittbot/bot.rb, line 308
def list_tasks
  tasks = $bot[:tasks].map do |name, value|
    [name, value[:desc]]
  end.to_h
  longest_name = tasks.keys.map(&:to_s).max { |a, b| a.length <=> b.length }.length
  tasks = tasks.map do |name, desc|
    "twittbot cron %-*s # %s" % [longest_name + 2, name, desc]
  end.sort

  puts tasks
end
load_special_tasks() click to toggle source

Load special tasks (e.g. list)

# File lib/twittbot/bot.rb, line 298
def load_special_tasks
  $bot[:tasks][:list] = {
    block: -> do
      list_tasks
    end,
    desc: 'List all available tasks'
  }
end