class Ebooks::Bot
Attributes
@return [String] OAuth access token from `ebooks auth`
@return [String] OAuth access secret from `ebooks auth`
@return [Array<String>] list of usernames to block on contact
@return [String] OAuth consumer key for a Twitter app
@return [String] OAuth consumer secret for a Twitter app
@return [Hash{String => Ebooks::Conversation}] maps tweet ids to their conversation contexts
@return [Range, Integer] range of seconds to delay in delay method
@return [Twitter::User] Twitter user object of bot
@return [String] Twitter username of bot
Public Class Methods
@return [Array] list of all defined bots
# File lib/bot_twitter_ebooks/bot.rb, line 165 def self.all; @@all ||= []; end
Fetches a bot by username @param username [String] @return [Ebooks::Bot]
# File lib/bot_twitter_ebooks/bot.rb, line 170 def self.get(username) all.find { |bot| bot.username.downcase == username.downcase } end
Initializes and configures bot @param args Arguments passed to configure method @param b Block to call with new bot
# File lib/bot_twitter_ebooks/bot.rb, line 183 def initialize(username, &b) @blacklist ||= [] @conversations ||= {} # Tweet ids we've already observed, to avoid duplication @seen_tweets ||= {} @username = username @delay_range ||= 1..6 configure b.call(self) unless b.nil? Bot.all << self end
Public Instance Methods
Check if a username is blacklisted @param username [String] @return [Boolean]
# File lib/bot_twitter_ebooks/bot.rb, line 386 def blacklisted?(username) if @blacklist.map(&:downcase).include?(username.downcase) true else false end end
# File lib/bot_twitter_ebooks/bot.rb, line 197 def configure raise ConfigurationError, "Please override the 'configure' method for subclasses of Ebooks::Bot." end
Find or create the conversation context for this tweet @param tweet [Twitter::Tweet] @return [Ebooks::Conversation]
# File lib/bot_twitter_ebooks/bot.rb, line 204 def conversation(tweet) conv = if tweet.in_reply_to_status_id? @conversations[tweet.in_reply_to_status_id] end if conv.nil? conv = @conversations[tweet.id] || Conversation.new(self) end if tweet.in_reply_to_status_id? @conversations[tweet.in_reply_to_status_id] = conv end @conversations[tweet.id] = conv # Expire any old conversations to prevent memory growth @conversations.each do |k,v| if v != conv && Time.now - v.last_update > 3600 @conversations.delete(k) end end conv end
Delay an action for a variable period of time @param range [Range, Integer] range of seconds to choose for delay
# File lib/bot_twitter_ebooks/bot.rb, line 377 def delay(range=@delay_range, &b) time = rand(range) unless range.is_a? Integer sleep time b.call end
Favorite a tweet @param tweet [Twitter::Tweet]
# File lib/bot_twitter_ebooks/bot.rb, line 424 def favorite(tweet) log "Favoriting @#{tweet.user.screen_name}: #{tweet.text}" begin twitter.favorite(tweet.id) rescue Twitter::Error::Forbidden log "Already favorited: #{tweet.user.screen_name}: #{tweet.text}" end end
Fire an event @param event [Symbol] event to fire @param args arguments for event handler
# File lib/bot_twitter_ebooks/bot.rb, line 368 def fire(event, *args) handler = "on_#{event}".to_sym if respond_to? handler self.send(handler, *args) end end
Follow a user @param user [String] username or user id
# File lib/bot_twitter_ebooks/bot.rb, line 448 def follow(user, *args) log "Following #{user}" twitter.follow(user, *args) end
Logs info to stdout in the context of this bot
# File lib/bot_twitter_ebooks/bot.rb, line 175 def log(*args) STDOUT.print "@#{@username}: " + args.map(&:to_s).join(' ') + "\n" STDOUT.flush end
Calculate some meta information about a tweet relevant for replying @param ev [Twitter::Tweet] @return [Ebooks::TweetMeta]
# File lib/bot_twitter_ebooks/bot.rb, line 251 def meta(ev) TweetMeta.new(self, ev) end
Tweet some text with an image @param txt [String] @param pic [String] filename
# File lib/bot_twitter_ebooks/bot.rb, line 476 def pictweet(txt, pic, *args) log "Tweeting #{txt.inspect} - #{pic} #{args}" twitter.update_with_media(txt, File.new(pic), *args) end
Configures client and fires startup event
# File lib/bot_twitter_ebooks/bot.rb, line 328 def prepare # Sanity check if @username.nil? raise ConfigurationError, "bot username cannot be nil" end if @consumer_key.nil? || @consumer_key.empty? || @consumer_secret.nil? || @consumer_key.empty? log "Missing consumer_key or consumer_secret. These details can be acquired by registering a Twitter app at https://apps.twitter.com/" exit 1 end if @access_token.nil? || @access_token.empty? || @access_token_secret.nil? || @access_token_secret.empty? log "Missing access_token or access_token_secret. Please run `ebooks auth`." exit 1 end # Save old name old_name = username # Load user object and actual username update_myself # Warn about mismatches unless it was clearly intentional log "warning: bot expected to be @#{old_name} but connected to @#{username}" unless username == old_name || old_name.empty? fire(:startup) end
Receive an event from the twitter stream @param ev [Object] Twitter streaming event
# File lib/bot_twitter_ebooks/bot.rb, line 257 def receive_event(ev) case ev when Array # Initial array sent on first connection log "Online!" fire(:connect, ev) return when Twitter::DirectMessage return if ev.sender.id == @user.id # Don't reply to self log "DM from @#{ev.sender.screen_name}: #{ev.text}" fire(:message, ev) when Twitter::Tweet return unless ev.text # If it's not a text-containing tweet, ignore it return if ev.user.id == @user.id # Ignore our own tweets if ev.retweet? && ev.retweeted_tweet.user.id == @user.id # Someone retweeted our tweet! fire(:retweet, ev) return end meta = meta(ev) if blacklisted?(ev.user.screen_name) log "Blocking blacklisted user @#{ev.user.screen_name}" @twitter.block(ev.user.screen_name) end # Avoid responding to duplicate tweets if @seen_tweets[ev.id] log "Not firing event for duplicate tweet #{ev.id}" return else @seen_tweets[ev.id] = true end if meta.mentions_bot? log "Mention from @#{ev.user.screen_name}: #{ev.text}" conversation(ev).add(ev) fire(:mention, ev) else fire(:timeline, ev) end when Twitter::Streaming::Event case ev.name when :follow return if ev.source.id == @user.id log "Followed by #{ev.source.screen_name}" fire(:follow, ev.source) when :favorite, :unfavorite return if ev.source.id == @user.id # Ignore our own favorites log "@#{ev.source.screen_name} #{ev.name.to_s}d: #{ev.target_object.text}" fire(ev.name, ev.source, ev.target_object) when :user_update update_myself ev.source end when Twitter::Streaming::DeletedTweet # Pass else log ev end end
Reply to a tweet or a DM. @param ev [Twitter::Tweet, Twitter::DirectMessage] @param text [String] contents of reply excluding reply_prefix @param opts [Hash] additional params to pass to twitter gem
# File lib/bot_twitter_ebooks/bot.rb, line 398 def reply(ev, text, opts={}) opts = opts.clone if ev.is_a? Twitter::DirectMessage log "Sending DM to @#{ev.sender.screen_name}: #{text}" twitter.create_direct_message(ev.sender.screen_name, text, opts) elsif ev.is_a? Twitter::Tweet meta = meta(ev) if conversation(ev).is_bot?(ev.user.screen_name) log "Not replying to suspected bot @#{ev.user.screen_name}" return false end text = meta.reply_prefix + text unless text.match(/@#{Regexp.escape ev.user.screen_name}/i) log "Replying to @#{ev.user.screen_name} with: #{text}" tweet = twitter.update(text, opts.merge(in_reply_to_status_id: ev.id)) conversation(tweet).add(tweet) tweet else raise Exception("Don't know how to reply to a #{ev.class}") end end
Retweet a tweet @param tweet [Twitter::Tweet]
# File lib/bot_twitter_ebooks/bot.rb, line 436 def retweet(tweet) log "Retweeting @#{tweet.user.screen_name}: #{tweet.text}" begin twitter.retweet(tweet.id) rescue Twitter::Error::Forbidden log "Already retweeted: #{tweet.user.screen_name}: #{tweet.text}" end end
Get a scheduler for this bot @return [Rufus::Scheduler]
# File lib/bot_twitter_ebooks/bot.rb, line 469 def scheduler @scheduler ||= Rufus::Scheduler.new end
Start running user event stream
# File lib/bot_twitter_ebooks/bot.rb, line 357 def start log "starting tweet stream" stream.user do |ev| receive_event ev end end
@return [Twitter::Streaming::Client] underlying streaming client from twitter gem
# File lib/bot_twitter_ebooks/bot.rb, line 239 def stream @stream ||= Twitter::Streaming::Client.new do |config| config.consumer_key = @consumer_key config.consumer_secret = @consumer_secret config.access_token = @access_token config.access_token_secret = @access_token_secret end end
Tweet something @param text [String]
# File lib/bot_twitter_ebooks/bot.rb, line 462 def tweet(text, *args) log "Tweeting '#{text}'" twitter.update(text, *args) end
@return [Twitter::REST::Client] underlying REST client from twitter gem
# File lib/bot_twitter_ebooks/bot.rb, line 229 def twitter @twitter ||= Twitter::REST::Client.new do |config| config.consumer_key = @consumer_key config.consumer_secret = @consumer_secret config.access_token = @access_token config.access_token_secret = @access_token_secret end end
Unfollow a user @param user [String] username or user id
# File lib/bot_twitter_ebooks/bot.rb, line 455 def unfollow(user, *args) log "Unfollowing #{user}" twitter.unfollow(user, *args) end
Updates @user and calls on_user_update.
# File lib/bot_twitter_ebooks/bot.rb, line 320 def update_myself(new_me=twitter.user) @user = new_me if @user.nil? || new_me.id == @user.id @username = @user.screen_name log 'User information updated' fire(:user_update) end