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



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

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`



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

def access_token
  @access_token
end

#access_token_secretString

Returns OAuth access secret from ‘ebooks auth`.

Returns:

  • (String)

    OAuth access secret from ‘ebooks auth`



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

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



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

def blacklist
  @blacklist
end

#consumer_keyString

Returns OAuth consumer key for a Twitter app.

Returns:

  • (String)

    OAuth consumer key for a Twitter app



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

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



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

def consumer_secret
  @consumer_secret
end

#conversationsHash{String => Ebooks::Conversation}

Returns maps tweet ids to their conversation contexts.

Returns:



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

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



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

def delay_range
  @delay_range
end

#usernameString

Returns Twitter username of bot.

Returns:

  • (String)

    Twitter username of bot



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

def username
  @username
end

Class Method Details

.allArray

Returns list of all defined bots.

Returns:

  • (Array)

    list of all defined bots



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

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

.get(username) ⇒ Ebooks::Bot

Fetches a bot by username

Parameters:

  • username (String)

Returns:



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

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)


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

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

#configureObject

Raises:



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

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:



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

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



351
352
353
354
355
# File 'lib/twitter_ebooks/bot.rb', line 351

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)


398
399
400
401
402
403
404
405
406
# File 'lib/twitter_ebooks/bot.rb', line 398

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



342
343
344
345
346
347
# File 'lib/twitter_ebooks/bot.rb', line 342

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



422
423
424
425
# File 'lib/twitter_ebooks/bot.rb', line 422

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



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

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:



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

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



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

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



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/twitter_ebooks/bot.rb', line 302

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



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
# File 'lib/twitter_ebooks/bot.rb', line 250

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) && 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.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



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/twitter_ebooks/bot.rb', line 372

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

    log "Replying to @#{ev.user.screen_name} with: #{meta.reply_prefix + text}"
    text = meta.reply_prefix + text unless text.match /@#{Regexp.escape ev.user.screen_name}/i
    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)


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

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)


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

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

#startObject

Start running user event stream



331
332
333
334
335
336
337
# File 'lib/twitter_ebooks/bot.rb', line 331

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



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

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)


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

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



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

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



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

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