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



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/twitter_ebooks/bot.rb', line 185

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`



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

def access_token
  @access_token
end

#access_token_secretString

Returns OAuth access secret from ‘ebooks auth`.

Returns:

  • (String)

    OAuth access secret from ‘ebooks auth`



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

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



160
161
162
# File 'lib/twitter_ebooks/bot.rb', line 160

def blacklist
  @blacklist
end

#consumer_keyString

Returns OAuth consumer key for a Twitter app.

Returns:

  • (String)

    OAuth consumer key for a Twitter app



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

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



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

def consumer_secret
  @consumer_secret
end

#conversationsHash{String => Ebooks::Conversation}

Returns maps tweet ids to their conversation contexts.

Returns:



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

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



164
165
166
# File 'lib/twitter_ebooks/bot.rb', line 164

def delay_range
  @delay_range
end

#userTwitter::User

Returns Twitter user object of bot.

Returns:

  • (Twitter::User)

    Twitter user object of bot



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

def user
  @user
end

#usernameString

Returns Twitter username of bot.

Returns:

  • (String)

    Twitter username of bot



158
159
160
# File 'lib/twitter_ebooks/bot.rb', line 158

def username
  @username
end

Class Method Details

.allArray

Returns list of all defined bots.

Returns:

  • (Array)

    list of all defined bots



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

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

.get(username) ⇒ Ebooks::Bot

Fetches a bot by username

Parameters:

  • username (String)

Returns:



172
173
174
# File 'lib/twitter_ebooks/bot.rb', line 172

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)


388
389
390
391
392
393
394
# File 'lib/twitter_ebooks/bot.rb', line 388

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

#configureObject

Raises:



199
200
201
# File 'lib/twitter_ebooks/bot.rb', line 199

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:



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/twitter_ebooks/bot.rb', line 206

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



379
380
381
382
383
# File 'lib/twitter_ebooks/bot.rb', line 379

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)


426
427
428
429
430
431
432
433
434
# File 'lib/twitter_ebooks/bot.rb', line 426

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



370
371
372
373
374
375
# File 'lib/twitter_ebooks/bot.rb', line 370

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



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

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



177
178
179
180
# File 'lib/twitter_ebooks/bot.rb', line 177

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:



253
254
255
# File 'lib/twitter_ebooks/bot.rb', line 253

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



478
479
480
481
# File 'lib/twitter_ebooks/bot.rb', line 478

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



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/twitter_ebooks/bot.rb', line 330

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

Receive an event from the twitter stream

Parameters:

  • ev (Object)

    Twitter streaming event



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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/twitter_ebooks/bot.rb', line 259

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 = {}) ⇒ 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



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/twitter_ebooks/bot.rb', line 400

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)


438
439
440
441
442
443
444
445
446
# File 'lib/twitter_ebooks/bot.rb', line 438

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)


471
472
473
# File 'lib/twitter_ebooks/bot.rb', line 471

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

#startObject

Start running user event stream



359
360
361
362
363
364
365
# File 'lib/twitter_ebooks/bot.rb', line 359

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



241
242
243
244
245
246
247
248
# File 'lib/twitter_ebooks/bot.rb', line 241

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)


464
465
466
467
# File 'lib/twitter_ebooks/bot.rb', line 464

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



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

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



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

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

#update_myself(new_me = twitter.user) ⇒ Object

Updates @user and calls on_user_update.



322
323
324
325
326
327
# File 'lib/twitter_ebooks/bot.rb', line 322

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