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



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

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



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

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:



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

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



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

def delay_range
  @delay_range
end

#userTwitter::User

Returns Twitter user object of bot.

Returns:

  • (Twitter::User)

    Twitter user object of bot



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

def user
  @user
end

#usernameString

Returns Twitter username of bot.

Returns:

  • (String)

    Twitter username of bot



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

def username
  @username
end

Class Method Details

.allArray

Returns list of all defined bots.

Returns:

  • (Array)

    list of all defined bots



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

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

.get(username) ⇒ Ebooks::Bot

Fetches a bot by username

Parameters:

  • username (String)

Returns:



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

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)


374
375
376
377
378
379
380
# File 'lib/twitter_ebooks/bot.rb', line 374

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

#configureObject

Raises:



191
192
193
# File 'lib/twitter_ebooks/bot.rb', line 191

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:



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

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



365
366
367
368
369
# File 'lib/twitter_ebooks/bot.rb', line 365

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)


412
413
414
415
416
417
418
419
420
# File 'lib/twitter_ebooks/bot.rb', line 412

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



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

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



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

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



169
170
171
172
# File 'lib/twitter_ebooks/bot.rb', line 169

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:



245
246
247
# File 'lib/twitter_ebooks/bot.rb', line 245

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



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

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



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/twitter_ebooks/bot.rb', line 316

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



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 251

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

    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



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/twitter_ebooks/bot.rb', line 386

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)


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

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)


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

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

#startObject

Start running user event stream



345
346
347
348
349
350
351
# File 'lib/twitter_ebooks/bot.rb', line 345

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



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

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)


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

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



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

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



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

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.



308
309
310
311
312
313
# File 'lib/twitter_ebooks/bot.rb', line 308

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