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'

  1
  2require 'rubygems'
  3require 'twitter_oauth'
  4require 'yaml'
  5#
  6# extend Hash class to turn keys into symbols
  7#
  8class Hash
  9  def symbolize_keys!
 10    replace(inject({}) do |hash,(key,value)|
 11      hash[key.to_sym] = value.is_a?(Hash) ? value.symbolize_keys! : value
 12      hash
 13    end)
 14  end
 15end
 16
 17#
 18# base class to handle being a twitter bot
 19#
 20class Skeleton
 21  attr_accessor :config
 22  attr_accessor :client
 23
 24  def debug(s)
 25    puts "***** #{s}"
 26  end
 27
 28  def run
 29    load_config
 30    login
 31    search
 32    replies
 33    update_config
 34  end
 35
 36
 37  def default_opts
 38    {
 39      :since_id => @config.has_key?(:since_id) ? @config[:since_id] : 0
 40    }
 41  end
 42
 43  # implement search in the extended class
 44  def search
 45
 46  end
 47
 48  # implement replies in the extended class
 49  def replies
 50
 51  end
 52
 53  # simple wrapper for sending a message
 54  def tweet(txt, params = {})
 55    debug txt
 56    @client.update txt, params
 57  end
 58
 59  # track the most recent msg we've handled
 60  def update_since_id(s)
 61    if @config[:since_id].nil? or s["id"] > @config[:since_id]
 62      @config[:since_id] = s["id"]
 63    end
 64  end
 65
 66protected
 67
 68  #
 69  # handle oauth for this request.  if the client isn't authorized, print
 70  # out the auth URL and get a pin code back from the user
 71  #
 72  def login
 73    @client = TwitterOAuth::Client.new(
 74                                      :consumer_key => @config[:consumer_key],
 75                                      :consumer_secret => @config[:consumer_secret],
 76                                      :token => @config[:token].nil? ? nil : @config[:token],
 77                                      :secret => @config[:secret].nil? ? nil : @config[:secret]
 78                                      )
 79
 80    if @config[:token].nil?
 81      request_token = @client.request_token
 82
 83      puts "#{request_token.authorize_url}\n"
 84      puts "Paste your PIN and hit enter when you have completed authorization."
 85      pin = STDIN.readline.chomp
 86
 87      access_token = @client.authorize(
 88                                      request_token.token,
 89                                      request_token.secret,
 90                                      :oauth_verifier => pin
 91                                      )
 92
 93      if @client.authorized?
 94        @config[:token] = access_token.token
 95        @config[:secret] = access_token.secret
 96        update_config
 97      else
 98        debug "OOPS"
 99        exit
100      end
101    end
102  end
103
104  #
105  # figure out what config file to load
106  #
107  def config_file
108    filename = "#{File.basename($0,".rb")}.yml"
109    debug "load config: #{filename}"
110    File.expand_path(filename)
111  end
112
113  def load_config
114    tmp = {}
115    begin
116      File.open( config_file ) { |yf|
117        tmp = YAML::load( yf )
118      }
119      tmp.symbolize_keys! if tmp
120    rescue Exception => err
121      debug err.message
122      tmp = {
123        :since_id => 0
124      }
125    end
126
127    # 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>
128    if ! tmp.has_key?(:consumer_key)
129      tmp[:consumer_key] = "hjaOOEeeMpJSqZR7dvhxjg"
130      tmp[:consumer_secret] = "wA5iqjfCf9aeGMMItqd6ylEEZAbcm7m6R7vVpaQV0s"
131    end
132
133    @config = tmp
134  end
135
136  # write out our config file
137  def update_config(tmp=@config)
138    # update datastore
139    File.open(config_file, 'w') { |f| YAML.dump(tmp, f) }
140  end
141end
142</script>
143
144

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

 1#!/usr/bin/ruby
 2require 'skeleton'
 3
 4class Surely < Skeleton
 5  def search
 6
 7    debug "check for tweets since #{@config[:since_id]}"
 8
 9    #
10    # search twitter
11    #
12    search = @client.search('surely you must be joking', default_opts)
13
14    if search != nil
15      if @config[:since_id].nil? or search["max_id"].to_i > @config[:since_id]
16        @config[:since_id] = search["max_id"].to_i
17      end
18
19      search["results"].each { |s|
20        begin
21          debug s["text"]
22          txt = "@#{s['from_user']} I am serious, and don't call me Shirley!"
23          tweet txt, :in_reply_to_status_id => s["id"]
24        rescue Exception => e
25        end
26      }
27    end
28  end
29end
30
31@sk = Surely.new
32@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