class Ebooks::Bot

Attributes

access_token[RW]

@return [String] OAuth access token from `ebooks auth`

access_token_secret[RW]

@return [String] OAuth access secret from `ebooks auth`

blacklist[RW]

@return [Array<String>] list of usernames to block on contact

consumer_key[RW]

@return [String] OAuth consumer key for a Twitter app

consumer_secret[RW]

@return [String] OAuth consumer secret for a Twitter app

conversations[RW]

@return [Hash{String => Ebooks::Conversation}] maps tweet ids to their conversation contexts

delay_range[RW]

@return [Range, Integer] range of seconds to delay in delay method

user[RW]

@return [Twitter::User] Twitter user object of bot

username[RW]

@return [String] Twitter username of bot

Public Class Methods

all() click to toggle source

@return [Array] list of all defined bots

# File lib/bot_twitter_ebooks/bot.rb, line 165
def self.all; @@all ||= []; end
get(username) click to toggle source

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
new(username, &b) click to toggle source

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

blacklisted?(username) click to toggle source

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
configure() click to toggle source
# File lib/bot_twitter_ebooks/bot.rb, line 197
def configure
  raise ConfigurationError, "Please override the 'configure' method for subclasses of Ebooks::Bot."
end
conversation(tweet) click to toggle source

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(range=@delay_range, &b) click to toggle source

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(tweet) click to toggle source

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(event, *args) click to toggle source

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(user, *args) click to toggle source

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
log(*args) click to toggle source

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
meta(ev) click to toggle source

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
pictweet(txt, pic, *args) click to toggle source

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
prepare() click to toggle source

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_event(ev) click to toggle source

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(ev, text, opts={}) click to toggle source

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(tweet) click to toggle source

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
scheduler() click to toggle source

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() click to toggle source

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
stream() click to toggle source

@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(text, *args) click to toggle source

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
twitter() click to toggle source

@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(user, *args) click to toggle source

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
update_myself(new_me=twitter.user) click to toggle source

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