Skip to content

Instantly share code, notes, and snippets.

@psobot
Last active January 26, 2018 14:41
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save psobot/1413715 to your computer and use it in GitHub Desktop.
Save psobot/1413715 to your computer and use it in GitHub Desktop.
"The Street Preacher" - hyper-local twitter bot
<a href="http://foursquare.com" rel="nofollow">foursquare</a>
<a href="http://instagram.com" rel="nofollow">Instagram</a>
JESUS!
BEEEEELIEVE!
THE LORD!
JEEEESUS!
PRAISE HIM!
BELIEVE IN THE LOOOORD!
GOD ALMIGHTY!
ONLY ONE WAY TO HOLY GOD!
BELIEVE! BELIEVE!
GOD'S LOVE!
# "The Street Preacher"
# hyper-local twitter bot
#
# by Peter Sobot (psobot.com)
# Created November 29, 2011
# Updated April 14, 2014
#
# ---------------------------
#
# Instructions:
# Place phrases in phrases.txt (one per line)
# Place excluded user names in excluded_users.txt (one per line)
# Place excluded source (Foursquare, etc.) in excluded_sources.txt (one per line)
# Set a latitude and longitude (@from_lat, @from_lng)
# Set a radius (currently in degrees lat/long)
# Set your twitter account's username (to prevent feedback)
#
# Enter your Twitter application keys and OAuth credentials
# (get them from https://dev.twitter.com/)
#
# Run:
# `ruby preacher.rb start`
#
# ???
#
# Profit! (Not really.)
#
# ---------------------------
#
# Defaults are set to Yonge & Dundas Square, Toronto.
# @yonge_dundas is a twitter clone of the notorious street preachers
# that live at that intersection.
#
# ---------------------------
require 'rubygems'
require 'tweetstream'
require 'twitter'
require 'logger'
PHRASES = Dir.pwd + '/phrases.txt'
EXCLUDED_USERS = Dir.pwd + '/excluded_users.txt'
EXCLUDED_SOURCES = Dir.pwd + '/excluded_sources.txt'
USER_TWEET_TIME_LIMIT = 10800 #in seconds, how often is too often? 3 hours.
SCREAM_TIME_LIMIT = 10800 #in seconds, how often is too often? 3 hours.
ERROR_TIMEOUT = 300 #in seconds, how long do we wait if Twitter barks at us?
# Set this to your account's username, so it doesn't feedback loop.
USERNAME = 'yonge_dundas'
@logger = Logger.new STDERR
# Twitter phrases:
@phrases = IO.readlines(PHRASES).collect{|p|p.chomp}.compact.reject{|n|n.empty?}
def random_phrase
@phrases.sort_by{ rand }.first
end
def excluded_users
IO.readlines(EXCLUDED_USERS).collect{|p|p.chomp}.compact.reject{|n|n.empty?}
end
def excluded_sources
IO.readlines(EXCLUDED_SOURCES).collect{|p|p.chomp}.compact.reject{|n|n.empty?}
end
# Let's store a list of people and times we've tweeted at them, to avoid spam
@user_cache = {}
@last_scream = nil
def hit_recently user_id
if @user_cache[user_id].nil?
false
elsif (Time.now - @user_cache[user_id]) < USER_TWEET_TIME_LIMIT
true
else
# Clear the cache for this user.
@user_cache.delete(user_id)
false
end
end
def screamed_recently
!@last_scream.nil? && (Time.now - @last_scream) < SCREAM_TIME_LIMIT
end
# Tweet from:
@from_lat = 43.65641564830964
@from_lng = -79.38105940818787
radius = 0.001 # catch area in degrees lat/lng
consumer_key = "EltYoAhkQXn8WJrm2Jqxw"
consumer_secret = "C1lG5QKAL2QJoY438MjyFS346DJuQ8ULmqxQDPC6POo"
oauth_token = "424533163-OH0NoCajDz5C8MbuxtC89GuUCKsudXwqph4fI3Fs"
oauth_token_secret = "5LYJvi9Qa8Duq8qs55DOAwkdzaTKNZeq6bYBkTklBf0"
# Confiruationses
@client = Twitter::REST::Client.new do |config|
config.consumer_key = consumer_key
config.consumer_secret = consumer_secret
config.access_token = oauth_token
config.access_token_secret = oauth_token_secret
end
TweetStream.configure do |config|
config.consumer_key = consumer_key
config.consumer_secret = consumer_secret
config.oauth_token = oauth_token
config.oauth_token_secret = oauth_token_secret
config.auth_method = :oauth
config.parser = :yajl
end
# Let's make us a bounding box to give Twitter's streaming API
N = @from_lat + radius
S = @from_lat - radius
E = @from_lng + radius
W = @from_lng - radius
def maybe_scream
if not screamed_recently
@logger.info "Screaming. Haven't screamed since " + @last_scream.inspect
tweet = @client.update(
random_phrase,
:lat => @from_lat,
:long => @from_lng,
:display_coordinates => true
)
@last_scream = Time.now
@logger.info "\t#{tweet.id}: \"#{tweet.text}\""
end
end
def parse_tweet status
@logger.info "Caught tweet with id #{status.id} from #{status.user.screen_name}"
if status.user.screen_name == USERNAME
@logger.info "Not replying to self!"
return maybe_scream
end
if excluded_users.include? status.user.screen_name
@logger.info "Not replying to user: #{status.user.screen_name}"
return maybe_scream
end
if excluded_sources.include? status.source
@logger.info "Not replying to user with source: #{status.source}"
return maybe_scream
end
if status.attrs[:geo] and status.attrs[:geo][:type] == 'Point'
lat, lng = status.attrs[:geo][:coordinates]
if lng < [E, W].max and lng > [E, W].min and lat < [N, S].max and lat > [N, S].min
send_reply status
else
km_away = Math.sqrt(((lat - @from_lat) * 111)**2 + ((lng - @from_lng) * 79)**2)
@logger.info "Tweet not within bounding box:\t#{km_away} km away."
maybe_scream
end
elsif !(status.attrs[:entities][:user_mentions].select{ |x| x[:screen_name] == USERNAME }.empty?)
send_reply status
end
rescue Exception => ex
@logger.error ex.message
@logger.error ex.backtrace.join "\n"
end
def send_reply status
@logger.info "Got one! Replying to @#{status.user.screen_name}:"
@logger.info "\t#{status.id}: \"#{status.text}\""
if not status.in_reply_to_user_id \
and not status.retweeted \
and status.attrs[:entities][:user_mentions].select{ |x| x[:screen_name] != USERNAME }.empty? \
and not hit_recently(status.user.id)
tweet = @client.update(
"@#{status.user.screen_name} #{random_phrase}",
:in_reply_to_status_id => status.id,
:lat => @from_lat,
:long => @from_lng,
:display_coordinates => true
)
@user_cache[status.user.id] = Time.now
@logger.info "\t#{tweet.id}: \"#{tweet.text}\""
else
@logger.info "Didn't reply - tweet was mention, retweet, reply, or spammy."
@logger.info "In reply to: " + status.in_reply_to_user_id.inspect
@logger.info "Retweeted? " + status.retweeted.inspect
@logger.info "Mentioned: " + status.attrs[:entities][:user_mentions].inspect
@logger.info "User last hit at: " + @user_cache[status.user.id].inspect
maybe_scream
end
end
client = TweetStream::Daemon.new('preacher', :log_output => true)
client.on_error { |message| @logger.error message }
client.on_reconnect { |timeout, retries| @logger.error "Reconnect: timeout = #{timeout}, retries = #{retries}" }
# Start filtering based on location
begin
@logger.info "Starting up the Street Preacher..."
@logger.info "Searching on coordinates: #{W},#{S},#{E},#{N}"
client.locations("#{W},#{S},#{E},#{N}", :track => "@#{USERNAME}") { |status| parse_tweet status }
rescue HTTP::Parser::Error => ex
# Although TweetStream should recover from
# disconnections, it fails to do so properly.
@logger.error "HTTP Parser error encountered - let's sleep for #{ERROR_TIMEOUT}s."
@logger.error ex.message
@logger.error ex.backtrace.join "\n"
sleep ERROR_TIMEOUT
retry
end
@MayaSharon
Copy link

do you have a tutorial on how to use the code... im new to this and getting pretty confused

@psobot
Copy link
Author

psobot commented Apr 11, 2012

It's definitely not super user-friendly, that's for sure.
You'll need to be running Ruby, and have the tweetstream, twitter and logger gems installed.
You'll also need to create two text files in the same directory as the script:

  • phrases.txt, containing one phrase per line (to be tweeted)
  • excluded_users.txt containing one username per line that shouldn't be tweeted at.

Then find the following variables in the script, and set them appropriately:

  • @from_lat should be the latitude of the center of the catchment area.
  • @from_lng should be the longitude (of the same).
  • radius is the radius from the center of the catchment area to tweet at.
  • consumer_key, consumer_secret are both taken from your Twitter developer account (http://developer.twitter.com/ if you don't already have one)
  • oauth_token, oauth_token_secret are an oauth token pair, again grabbed from your Twitter dev site. This will make the bot tweet as the user who generated the token, so be careful who generates it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment