mirror of
https://github.com/thewesker/ebooks_example.git
synced 2025-12-20 04:11:13 -05:00
First commit for Boodoo_Books on _ebooks 3.0.0
- Use .env file for secrets/config values - Maintain follower/following parity periodically TODO: - Support banned terms (override #valid_tweet?) - Commands via DM (at least "tweet" command from owner should work) - Investigate (anti)social behavior more deeply THEN: - Spooling/wait-for-config state for post-deploy, pre-config - Archive/model creation post-deploy - Archive/model periodic refresh
This commit is contained in:
3
Gemfile
3
Gemfile
@@ -1,4 +1,5 @@
|
|||||||
source 'http://rubygems.org'
|
source 'http://rubygems.org'
|
||||||
ruby '2.1.3'
|
ruby '2.1.3'
|
||||||
|
|
||||||
gem 'twitter_ebooks'
|
gem 'twitter_ebooks 3.0.0'
|
||||||
|
gem 'dotenv'
|
||||||
|
|||||||
86
boodoo.rb
Normal file
86
boodoo.rb
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
require 'twitter_ebooks'
|
||||||
|
include Ebooks
|
||||||
|
|
||||||
|
module Ebooks::Boodoo
|
||||||
|
# supports Ruby Range literal, Fixnum, or Float as string
|
||||||
|
def parse_num(value)
|
||||||
|
eval(value.to_s[/^\d+(?:\.{1,3})?\d*$/].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make expected/possible Range
|
||||||
|
def parse_range(value)
|
||||||
|
value = parse_num(value)
|
||||||
|
if value.nil?
|
||||||
|
value = nil
|
||||||
|
elsif !value.respond_to?(:to_a)
|
||||||
|
value = Range.new(value, value)
|
||||||
|
end
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_array(value, array_splitter=nil)
|
||||||
|
array_splitter ||= / *[,;]+ */
|
||||||
|
value.split(array_splitter).map(&:strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Ebooks::TweetMeta
|
||||||
|
def is_retweet?
|
||||||
|
tweet.retweeted_status? || !!tweet.text[/^[RM]T[: ]/i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Ebooks::Boodoo::BoodooBot < Ebooks::Bot
|
||||||
|
# A rough error-catch/retry for rate limit, dupe fave, server timeouts
|
||||||
|
def catch_twitter
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
rescue Twitter::Error => error
|
||||||
|
@retries += 1
|
||||||
|
raise if @retries > @max_error_retries
|
||||||
|
if error.class == Twitter::Error::TooManyRequests
|
||||||
|
reset_in = error.rate_limit.reset_in
|
||||||
|
log "RATE: Going to sleep for ~#{reset_in / 60} minutes..."
|
||||||
|
sleep reset_in
|
||||||
|
retry
|
||||||
|
elsif error.class == Twitter::Error::Forbidden
|
||||||
|
# don't count "Already faved/followed" message against attempts
|
||||||
|
@retries -= 1 if error.to_s.include?("already")
|
||||||
|
log "WARN: #{error.to_s}"
|
||||||
|
return true
|
||||||
|
elsif ["execution", "capacity"].any?(&error.to_s.method(:include?))
|
||||||
|
log "ERR: Timeout?\n\t#{error}\nSleeping for #{@timeout_sleep} seconds..."
|
||||||
|
sleep @timeout_sleep
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
log "Unhandled exception from Twitter: #{error.to_s}"
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Override Ebooks::Bot#blacklisted? to ensure lower<=>lower check
|
||||||
|
def blacklisted?(username)
|
||||||
|
if @blacklist.map(&:downcase).include?(username.downcase)
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Follow new followers, unfollow lost followers
|
||||||
|
def follow_parity
|
||||||
|
followers = catch_twitter { twitter.followers(:count=>200).map(&:screen_name) }
|
||||||
|
following = catch_twitter { twitter.following(:count=>200).map(&:screen_name) }
|
||||||
|
to_follow = followers - following
|
||||||
|
to_unfollow = following - followers
|
||||||
|
twitter.follow(to_follow) unless to_follow.empty?
|
||||||
|
twitter.unfollow(to_unfollow) unless to_unfollow.empty?
|
||||||
|
@followers = followers
|
||||||
|
@following = following - to_unfollow
|
||||||
|
if !(to_follow.empty? || to_unfollow.empty?)
|
||||||
|
log "Followed #{to_follow.size}; unfollowed #{to_unfollow.size}."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
102
bots.rb
102
bots.rb
@@ -1,4 +1,11 @@
|
|||||||
require 'twitter_ebooks'
|
require 'twitter_ebooks'
|
||||||
|
require_relative 'boodoo'
|
||||||
|
require 'dotenv'
|
||||||
|
|
||||||
|
include Ebooks::Boodoo
|
||||||
|
|
||||||
|
# Read .env file and set values:
|
||||||
|
SETTINGS = Dotenv.load
|
||||||
|
|
||||||
# Information about a particular Twitter user we know
|
# Information about a particular Twitter user we know
|
||||||
class UserInfo
|
class UserInfo
|
||||||
@@ -10,42 +17,87 @@ class UserInfo
|
|||||||
# @param username [String]
|
# @param username [String]
|
||||||
def initialize(username)
|
def initialize(username)
|
||||||
@username = username
|
@username = username
|
||||||
@pesters_left = 1
|
@pesters_left = parse_num(SETTINGS['PESTER_COUNT']) || 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class CloneBot < Ebooks::Bot
|
class CloneBot < BoodooBot
|
||||||
attr_accessor :original, :model, :model_path
|
attr_accessor :original, :model, :model_path, :auth_name
|
||||||
|
# alias_method :oauth_token, :access_token
|
||||||
|
# alias_method :oauth_token_secret, :access_token_secret
|
||||||
def configure
|
def configure
|
||||||
# Configuration for all CloneBots
|
# create attr_accessors for all SETTINGS fields
|
||||||
self.consumer_key = ""
|
SETTINGS.keys.map(&:to_s).map(&:downcase).each(&Ebooks::Bot.method(:attr_accessor))
|
||||||
self.consumer_secret = ""
|
|
||||||
self.blacklist = ['kylelehk', 'friedrichsays', 'Sudieofna', 'tnietzschequote', 'NerdsOnPeriod', 'FSR', 'BafflingQuotes', 'Obey_Nxme']
|
|
||||||
|
|
||||||
|
# String fields taken as-is:
|
||||||
|
@consumer_key = SETTINGS['CONSUMER_KEY']
|
||||||
|
@consumer_secret = SETTINGS['CONSUMER_SECRET']
|
||||||
|
@access_token = SETTINGS['ACCESS_TOKEN']
|
||||||
|
@access_token_secret =SETTINGS['ACCESS_TOKEN_SECRET']
|
||||||
|
@tweet_interval = SETTINGS['TWEET_INTERVAL']
|
||||||
|
# @pester_period = SETTINGS['PESTER_PERIOD']
|
||||||
|
|
||||||
|
# String fields forced to downcase:
|
||||||
|
@bot_name = SETTINGS['BOT_NAME'].downcase
|
||||||
|
@original = SETTINGS['SOURCE_USERNAME'].downcase
|
||||||
|
|
||||||
|
# Array fields are CSV or SSV
|
||||||
|
@blacklist = parse_array(SETTINGS['BLACKLIST'])
|
||||||
|
@banned_terms = parse_array(SETTINGS['BANNED_TERMS'])
|
||||||
|
@special_terms = parse_array(SETTINGS['SPECIAL_TERMS'])
|
||||||
|
|
||||||
|
# Fields parsed as Fixnum, Float, or Range:
|
||||||
|
@default_delay = parse_range(SETTINGS['DEFAULT_DELAY'])
|
||||||
|
@dm_delay = parse_range(SETTINGS['DM_DELAY']) || parse_range(SETTINGS['DEFAULT_DELAY'])
|
||||||
|
@mention_delay = parse_range(SETTINGS['MENTION_DELAY']) || parse_range(SETTINGS['DEFAULT_DELAY'])
|
||||||
|
@timeline_delay = parse_range(SETTINGS['TIMELINE_DELAY']) || parse_range(SETTINGS['DEFAULT_DELAY'])
|
||||||
|
@tweet_chance = parse_num(SETTINGS['TWEET_CHANCE'])
|
||||||
|
# @pester_count = parse_num(SETTINGS['PESTER_COUNT'])
|
||||||
|
@timeout_sleep = parse_num(SETTINGS['TIMEOUT_SLEEP'])
|
||||||
|
|
||||||
|
# from example
|
||||||
@userinfo = {}
|
@userinfo = {}
|
||||||
|
|
||||||
|
# added for BooDoo variant
|
||||||
|
@attempts = 0
|
||||||
|
@followers = []
|
||||||
|
@following = []
|
||||||
|
# @have_talked = {}
|
||||||
|
|
||||||
|
# load model file
|
||||||
load_model!
|
load_model!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def top100; @top100 ||= model.keywords.take(100); end
|
def top100; @top100 ||= model.keywords.take(100); end
|
||||||
def top20; @top20 ||= model.keywords.take(20); end
|
def top20; @top20 ||= model.keywords.take(20); end
|
||||||
|
|
||||||
def delay(&b)
|
def delay(d, &b)
|
||||||
sleep (1..4).to_a.sample
|
d ||= default_delay
|
||||||
|
sleep (d || [0]).to_a.sample
|
||||||
b.call
|
b.call
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_startup
|
def on_startup
|
||||||
scheduler.cron '0 0 * * *' do
|
log "I started up!"
|
||||||
# Each day at midnight, post a single tweet
|
scheduler.interval @tweet_interval do
|
||||||
tweet(model.make_statement)
|
if rand < @tweet_chance
|
||||||
|
tweet(model.make_statement)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scheduler.interval @update_follows_interval do
|
||||||
|
follow_parity
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: This throws a weird error.
|
||||||
|
# Probably don't need it anyway?
|
||||||
|
# @auth_name ||= twitter.user.screen_name
|
||||||
|
# log "Logged in as #{auth_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_direct_message(dm)
|
def on_direct_message(dm)
|
||||||
delay do
|
# TODO: Add controls here! Especially "tweet"
|
||||||
|
delay(dm_delay) do
|
||||||
reply(dm, model.make_response(dm.text))
|
reply(dm, model.make_response(dm.text))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -54,7 +106,7 @@ class CloneBot < Ebooks::Bot
|
|||||||
# Become more inclined to pester a user when they talk to us
|
# Become more inclined to pester a user when they talk to us
|
||||||
userinfo(tweet.user.screen_name).pesters_left += 1
|
userinfo(tweet.user.screen_name).pesters_left += 1
|
||||||
|
|
||||||
delay do
|
delay(mention_delay) do
|
||||||
reply(tweet, model.make_response(meta(tweet).mentionless, meta(tweet).limit))
|
reply(tweet, model.make_response(meta(tweet).mentionless, meta(tweet).limit))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -62,13 +114,13 @@ class CloneBot < Ebooks::Bot
|
|||||||
def on_timeline(tweet)
|
def on_timeline(tweet)
|
||||||
return if tweet.retweeted_status?
|
return if tweet.retweeted_status?
|
||||||
return unless can_pester?(tweet.user.screen_name)
|
return unless can_pester?(tweet.user.screen_name)
|
||||||
|
|
||||||
tokens = Ebooks::NLP.tokenize(tweet.text)
|
tokens = Ebooks::NLP.tokenize(tweet.text)
|
||||||
|
|
||||||
interesting = tokens.find { |t| top100.include?(t.downcase) }
|
interesting = tokens.find { |t| top100.include?(t.downcase) }
|
||||||
very_interesting = tokens.find_all { |t| top20.include?(t.downcase) }.length > 2
|
very_interesting = tokens.find_all { |t| top20.include?(t.downcase) }.length > 2
|
||||||
|
|
||||||
delay do
|
delay(timeline_delay) do
|
||||||
if very_interesting
|
if very_interesting
|
||||||
favorite(tweet) if rand < 0.5
|
favorite(tweet) if rand < 0.5
|
||||||
retweet(tweet) if rand < 0.1
|
retweet(tweet) if rand < 0.1
|
||||||
@@ -97,7 +149,7 @@ class CloneBot < Ebooks::Bot
|
|||||||
# Only follow our original user or people who are following our original user
|
# Only follow our original user or people who are following our original user
|
||||||
# @param user [Twitter::User]
|
# @param user [Twitter::User]
|
||||||
def can_follow?(username)
|
def can_follow?(username)
|
||||||
@original.nil? || username == @original || twitter.friendship?(username, @original)
|
@original.nil? || username == @original || twitter.friendship?(username, @original) || twitter.friendship?(username, @original) || twitter.friendship?(username, auth_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def favorite(tweet)
|
def favorite(tweet)
|
||||||
@@ -116,7 +168,7 @@ class CloneBot < Ebooks::Bot
|
|||||||
log "Not following @#{user.screen_name}"
|
log "Not following @#{user.screen_name}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def load_model!
|
def load_model!
|
||||||
return if @model
|
return if @model
|
||||||
@@ -128,9 +180,9 @@ class CloneBot < Ebooks::Bot
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CloneBot.new("abby_ebooks") do |bot|
|
|
||||||
bot.access_token = ""
|
|
||||||
bot.access_token_secret = ""
|
|
||||||
|
|
||||||
bot.original = "0xabad1dea"
|
|
||||||
|
CloneBot.new(SETTINGS['BOT_NAME']) do |bot|
|
||||||
|
# CloneBot#configure does everything!
|
||||||
|
bot
|
||||||
end
|
end
|
||||||
|
|||||||
18
sample.env
Normal file
18
sample.env
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BOT_NAME=
|
||||||
|
SOURCE_USERNAME=
|
||||||
|
CONSUMER_KEY=
|
||||||
|
CONSUMER_SECRET=
|
||||||
|
ACCESS_TOKEN=
|
||||||
|
ACCESS_TOKEN_SECRET=
|
||||||
|
UPDATE_FOLLOWS_INTERVAL=90m
|
||||||
|
TWEET_INTERVAL=2m
|
||||||
|
TWEET_CHANCE=0.8
|
||||||
|
BLACKLIST=tinysubversions, dril
|
||||||
|
BANNED_TERMS=voldemort, evgeny morozov, heroku
|
||||||
|
SPECIAL_TERMS=
|
||||||
|
DEFAULT_DELAY=5..60
|
||||||
|
DM_DELAY=0
|
||||||
|
MENTION_DELAY=5..60
|
||||||
|
TIMELINE_DELAY=10..600
|
||||||
|
TIMEOUT_SLEEP=5
|
||||||
|
MAX_ERROR_RETRIES=10
|
||||||
Reference in New Issue
Block a user