Class: Ebooks::Bot

Inherits:
Object
  • Object
show all
Defined in:
lib/twitter_ebooks/bot.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(username, &b) ⇒ Bot

Initializes and configures bot

Parameters:

  • args

    Arguments passed to configure method

  • b

    Block to call with new bot



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/twitter_ebooks/bot.rb', line 175

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

Instance Attribute Details

#access_tokenString

Returns OAuth access token from ‘ebooks auth`.

Returns:

  • (String)

    OAuth access token from ‘ebooks auth`



144
145
146
# File 'lib/twitter_ebooks/bot.rb', line 144

def access_token
  @access_token
end

#access_token_secretString

Returns OAuth access secret from ‘ebooks auth`.

Returns:

  • (String)

    OAuth access secret from ‘ebooks auth`



146
147
148
# File 'lib/twitter_ebooks/bot.rb', line 146

def access_token_secret
  @access_token_secret
end

#blacklistArray<String>

Returns list of usernames to block on contact.

Returns:

  • (Array<String>)

    list of usernames to block on contact



150
151
152
# File 'lib/twitter_ebooks/bot.rb', line 150

def blacklist
  @blacklist
end

#consumer_keyString

Returns OAuth consumer key for a Twitter app.

Returns:

  • (String)

    OAuth consumer key for a Twitter app



140
141
142
# File 'lib/twitter_ebooks/bot.rb', line 140

def consumer_key
  @consumer_key
end

#consumer_secretString

Returns OAuth consumer secret for a Twitter app.

Returns:

  • (String)

    OAuth consumer secret for a Twitter app



142
143
144
# File 'lib/twitter_ebooks/bot.rb', line 142

def consumer_secret
  @consumer_secret
end

#conversationsHash{String => Ebooks::Conversation}

Returns maps tweet ids to their conversation contexts.

Returns:



152
153
154
# File 'lib/twitter_ebooks/bot.rb', line 152

def conversations
  @conversations
end

#delay_rangeRange, Integer

Returns range of seconds to delay in delay method.

Returns:

  • (Range, Integer)

    range of seconds to delay in delay method



154
155
156
# File 'lib/twitter_ebooks/bot.rb', line 154

def delay_range
  @delay_range
end

#usernameString

Returns Twitter username of bot.

Returns:

  • (String)

    Twitter username of bot



148
149
150
# File 'lib/twitter_ebooks/bot.rb', line 148

def username
  @username
end

Class Method Details

.allArray

Returns list of all defined bots.

Returns:

  • (Array)

    list of all defined bots



157
# File 'lib/twitter_ebooks/bot.rb', line 157

def self.all; @@all ||= []; end

.get(username) ⇒ Ebooks::Bot

Fetches a bot by username

Parameters:

  • username (String)

Returns:



162
163
164
# File 'lib/twitter_ebooks/bot.rb', line 162

def self.get(username)
  all.find { |bot| bot.username == username }
end

Instance Method Details

#blacklisted?(username) ⇒ Boolean

Check if a username is blacklisted

Parameters:

  • username (String)

Returns:

  • (Boolean)


366
367
368
369
370
371
372
# File 'lib/twitter_ebooks/bot.rb', line 366

def blacklisted?(username)
  if @blacklist.map(&:downcase).include?(username.downcase)
    true
  else
    false
  end
end

#configureObject

Raises:



189
190
191
# File 'lib/twitter_ebooks/bot.rb', line 189

def configure
  raise ConfigurationError, "Please override the 'configure' method for subclasses of Ebooks::Bot."
end

#conversation(tweet) ⇒ Ebooks::Conversation

Find or create the conversation context for this tweet

Parameters:

  • tweet (Twitter::Tweet)

Returns:



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/twitter_ebooks/bot.rb', line 196

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) ⇒ Object

Delay an action for a variable period of time

Parameters:

  • range (Range, Integer) (defaults to: @delay_range)

    range of seconds to choose for delay



357
358
359
360
361
# File 'lib/twitter_ebooks/bot.rb', line 357

def delay(range=@delay_range, &b)
  time = range.to_a.sample unless range.is_a? Integer
  sleep time
  b.call
end

#favorite(tweet) ⇒ Object

Favorite a tweet

Parameters:

  • tweet (Twitter::Tweet)


404
405
406
407
408
409
410
411
412
# File 'lib/twitter_ebooks/bot.rb', line 404

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) ⇒ Object

Fire an event

Parameters:

  • event (Symbol)

    event to fire

  • args

    arguments for event handler



348
349
350
351
352
353
# File 'lib/twitter_ebooks/bot.rb', line 348

def fire(event, *args)
  handler = "on_#{event}".to_sym
  if respond_to? handler
    self.send(handler, *args)
  end
end

#follow(user, *args) ⇒ Object

Follow a user

Parameters:

  • user (String)

    username or user id



428
429
430
431
# File 'lib/twitter_ebooks/bot.rb', line 428

def follow(user, *args)
  log "Following #{user}"
  twitter.follow(user, *args)
end

#log(*args) ⇒ Object

Logs info to stdout in the context of this bot



167
168
169
170
# File 'lib/twitter_ebooks/bot.rb', line 167

def log(*args)
  STDOUT.print "@#{@username}: " + args.map(&:to_s).join(' ') + "\n"
  STDOUT.flush
end

#meta(ev) ⇒ Ebooks::TweetMeta

Calculate some meta information about a tweet relevant for replying

Parameters:

  • ev (Twitter::Tweet)

Returns:



243
244
245
# File 'lib/twitter_ebooks/bot.rb', line 243

def meta(ev)
  TweetMeta.new(self, ev)
end

#pictweet(txt, pic, *args) ⇒ Object

Tweet some text with an image

Parameters:

  • txt (String)
  • pic (String)

    filename



456
457
458
459
# File 'lib/twitter_ebooks/bot.rb', line 456

def pictweet(txt, pic, *args)
  log "Tweeting #{txt.inspect} - #{pic} #{args}"
  twitter.update_with_media(txt, File.new(pic), *args)
end

#prepareObject

Configures client and fires startup event



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/twitter_ebooks/bot.rb', line 308

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

  real_name = twitter.user.screen_name

  if real_name != @username
    log "connected to @#{real_name}-- please update config to match Twitter account name"
    @username = real_name
  end

  fire(:startup)
end

#receive_event(ev) ⇒ Object

Receive an event from the twitter stream

Parameters:

  • ev (Object)

    Twitter streaming event



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/twitter_ebooks/bot.rb', line 249

def receive_event(ev)
  if ev.is_a? Array # Initial array sent on first connection
    log "Online!"
    return
  end

  if ev.is_a? Twitter::DirectMessage
    return if ev.sender.screen_name.downcase == @username.downcase # Don't reply to self
    log "DM from @#{ev.sender.screen_name}: #{ev.text}"
    fire(:message, ev)

  elsif ev.respond_to?(:name)
    if ev.name == :follow
      return if ev.source.screen_name.downcase == @username.downcase
      log "Followed by #{ev.source.screen_name}"
      fire(:follow, ev.source)

    elsif ev.name == :favorite || ev.name == :unfavorite
      return if ev.source.screen_name.downcase == @username.downcase # 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)
    end

  elsif ev.is_a? Twitter::Tweet
    return unless ev.text # If it's not a text-containing tweet, ignore it
    return if ev.user.screen_name.downcase == @username.downcase # Ignore our own tweets

    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

  elsif ev.is_a?(Twitter::Streaming::DeletedTweet) ||
        ev.is_a?(Twitter::Streaming::Event)
    # pass
  else
    log ev
  end
end

#reply(ev, text, opts = {}) ⇒ Object

Reply to a tweet or a DM.

Parameters:

  • ev (Twitter::Tweet, Twitter::DirectMessage)
  • text (String)

    contents of reply excluding reply_prefix

  • opts (Hash) (defaults to: {})

    additional params to pass to twitter gem



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/twitter_ebooks/bot.rb', line 378

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) ⇒ Object

Retweet a tweet

Parameters:

  • tweet (Twitter::Tweet)


416
417
418
419
420
421
422
423
424
# File 'lib/twitter_ebooks/bot.rb', line 416

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

#schedulerRufus::Scheduler

Get a scheduler for this bot

Returns:

  • (Rufus::Scheduler)


449
450
451
# File 'lib/twitter_ebooks/bot.rb', line 449

def scheduler
  @scheduler ||= Rufus::Scheduler.new
end

#startObject

Start running user event stream



337
338
339
340
341
342
343
# File 'lib/twitter_ebooks/bot.rb', line 337

def start
  log "starting tweet stream"

  stream.user do |ev|
    receive_event ev
  end
end

#streamTwitter::Streaming::Client

Returns underlying streaming client from twitter gem.

Returns:

  • (Twitter::Streaming::Client)

    underlying streaming client from twitter gem



231
232
233
234
235
236
237
238
# File 'lib/twitter_ebooks/bot.rb', line 231

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) ⇒ Object

Tweet something

Parameters:

  • text (String)


442
443
444
445
# File 'lib/twitter_ebooks/bot.rb', line 442

def tweet(text, *args)
  log "Tweeting '#{text}'"
  twitter.update(text, *args)
end

#twitterTwitter::REST::Client

Returns underlying REST client from twitter gem.

Returns:

  • (Twitter::REST::Client)

    underlying REST client from twitter gem



221
222
223
224
225
226
227
228
# File 'lib/twitter_ebooks/bot.rb', line 221

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) ⇒ Object

Unfollow a user

Parameters:

  • user (String)

    username or user id



435
436
437
438
# File 'lib/twitter_ebooks/bot.rb', line 435

def unfollow(user, *args)
  log "Unfollowing #{user}"
  twitter.unfollow(user, *args)
end