Twitter Bots, now with OAuth Goodness

The one devoted reader of this blog (Googlebot, I'm looking at you) probably remembers that I have a couple of bots running on Twitter. Originally I was using a library called Twibot, which was nice, but never quite worked the way I wanted it to. So eventually, I ended up with my own very simple code.

I just finally updated my bots to authenticate via OAuth, a couple days before the deadline. While I was at it, I refactored most of the code into its own class, which the bots extend to add actual functionality. Here's the base class, which I call 'Skeleton'


require 'rubygems'
require 'twitter_oauth'
require 'yaml'
#
# extend Hash class to turn keys into symbols
#
class Hash
  def symbolize_keys!
    replace(inject({}) do |hash,(key,value)|
      hash[key.to_sym] = value.is_a?(Hash) ? value.symbolize_keys! : value
      hash
    end)
  end
end

#
# base class to handle being a twitter bot
#
class Skeleton
  attr_accessor :config
  attr_accessor :client

  def debug(s)
    puts "***** #{s}"
  end

  def run
    load_config
    login
    search
    replies
    update_config
  end


  def default_opts
    {
      :since_id => @config.has_key?(:since_id) ? @config[:since_id] : 0
    }
  end

  # implement search in the extended class
  def search

  end

  # implement replies in the extended class
  def replies

  end

  # simple wrapper for sending a message
  def tweet(txt, params = {})
    debug txt
    @client.update txt, params
  end

  # track the most recent msg we've handled
  def update_since_id(s)
    if @config[:since_id].nil? or s["id"] > @config[:since_id]
      @config[:since_id] = s["id"]
    end
  end

protected

  #
  # handle oauth for this request.  if the client isn't authorized, print
  # out the auth URL and get a pin code back from the user
  #
  def login
    @client = TwitterOAuth::Client.new(
                                      :consumer_key => @config[:consumer_key],
                                      :consumer_secret => @config[:consumer_secret],
                                      :token => @config[:token].nil? ? nil : @config[:token],
                                      :secret => @config[:secret].nil? ? nil : @config[:secret]
                                      )

    if @config[:token].nil?
      request_token = @client.request_token

      puts "#{request_token.authorize_url}\n"
      puts "Paste your PIN and hit enter when you have completed authorization."
      pin = STDIN.readline.chomp

      access_token = @client.authorize(
                                      request_token.token,
                                      request_token.secret,
                                      :oauth_verifier => pin
                                      )

      if @client.authorized?
        @config[:token] = access_token.token
        @config[:secret] = access_token.secret
        update_config
      else
        debug "OOPS"
        exit
      end
    end
  end

  #
  # figure out what config file to load
  #
  def config_file
    filename = "#{File.basename($0,".rb")}.yml"
    debug "load config: #{filename}"
    File.expand_path(filename)
  end

  def load_config
    tmp = {}
    begin
      File.open( config_file ) { |yf|
        tmp = YAML::load( yf )
      }
      tmp.symbolize_keys! if tmp
    rescue Exception => err
      debug err.message
      tmp = {
        :since_id => 0
      }
    end

    # defaults for now, obviously a big hack.  this is for botly, at <a href="http://dev.twitter.com/apps/207151">http://dev.twitter.com/apps/207151</a>
    if ! tmp.has_key?(:consumer_key)
      tmp[:consumer_key] = "hjaOOEeeMpJSqZR7dvhxjg"
      tmp[:consumer_secret] = "wA5iqjfCf9aeGMMItqd6ylEEZAbcm7m6R7vVpaQV0s"
    end

    @config = tmp
  end

  # write out our config file
  def update_config(tmp=@config)
    # update datastore
    File.open(config_file, 'w') { |f| YAML.dump(tmp, f) }
  end
end
</script>

And here's the actual code for my newest bot @dr_rumack:

#!/usr/bin/ruby
require 'skeleton'

class Surely < Skeleton
  def search

    debug "check for tweets since #{@config[:since_id]}"

    #
    # search twitter
    #
    search = @client.search('surely you must be joking', default_opts)

    if search != nil
      if @config[:since_id].nil? or search["max_id"].to_i > @config[:since_id]
        @config[:since_id] = search["max_id"].to_i
      end

      search["results"].each { |s|
        begin
          debug s["text"]
          txt = "@#{s['from_user']} I am serious, and don't call me Shirley!"
          tweet txt, :in_reply_to_status_id => s["id"]
        rescue Exception => e
        end
      }
    end
  end
end

@sk = Surely.new
@sk.run

Feel free to adapt this code in any way. I'd love to hear of any uses of it. I've thought about making it work more like twibot at some point, if there's any interest.

Filed under: ruby, twitter