Last active
January 26, 2018 14:41
-
-
Save psobot/1413715 to your computer and use it in GitHub Desktop.
"The Street Preacher" - hyper-local twitter bot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<a href="http://foursquare.com" rel="nofollow">foursquare</a> | |
<a href="http://instagram.com" rel="nofollow">Instagram</a> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
JESUS! | |
BEEEEELIEVE! | |
THE LORD! | |
JEEEESUS! | |
PRAISE HIM! | |
BELIEVE IN THE LOOOORD! | |
GOD ALMIGHTY! | |
ONLY ONE WAY TO HOLY GOD! | |
BELIEVE! BELIEVE! | |
GOD'S LOVE! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# "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 |
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
do you have a tutorial on how to use the code... im new to this and getting pretty confused