require 'rubygems' require 'twitter' require 'tlsmail' require 'optparse' require 'cgi' require 'net/pop' require 'rmail' require 'logger.rb' if RUBY_VERSION < "1.9" raise "Version too low. Please get Ruby 1.9" end module TwitterSms SEC_PER_HOUR = 3600 # Seconds per hour (don't like this...) # RMail doesn't like parsing body properly if there is a mistaken ' ' between body and header def self.space_stripper!(msg) msg.gsub!(/^\s+$/,'') end class Checker attr_reader :config def initialize(config_file="#{ENV['HOME']}/.twitter-sms.conf", args=ARGV) load_config(config_file) load_opts(args) # Loaded options are intended to overide defaults # Required for gmail smtp (and not a part of standalone Net::SMTP) Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE) end # Run the twitter-sms bot once or repeatedly, depending on config def run begin # do-while load_config if config_stale? unless @config['dont_refresh'] receive_messages if @config['active'] tweets = update send tweets unless tweets.nil? end if @config['keep_alive'] putd "Waiting for ~#{@config['wait']/60.0} minutes..." Kernel.sleep @config['wait'] end end while @config['keep_alive'] tweets # Return the last set of tweets we get end # Collect a list of recent tweets on a user's timeline (that are not their own) def update twitter = Twitter::Base.new(@user['name'],@user['password']) begin if twitter.rate_limit_status.remaining_hits > 0 now = Time.now tweets = twitter.timeline(:friends, :since => @last_check) @last_check = now else putd "Your account has run out of API calls; call not made." end rescue putd "Error occured retreiving timeline. Perhaps Internet is down?" end return tweets end def reduce_tweets(tweets) # Block own tweets if specified via settings tweets.reject! {|t| t.user.screen_name == @user['name']} unless @config['own_tweets'] # Don't send messages from users under no_follow tweets.reject! {|t| @config['no_follow'].member?(t.user.screen_name) } tweets.reverse # reverse-chronological end # Email via Gmail SMTP any tweets to the desired cell phone def send(tweets) putd "Sending received tweets..." begin # Start smtp connection to provided bot email Net::SMTP.start('smtp.gmail.com', 587, 'gmail.com', @bot['email'], @bot['password'], :login) do |smtp| tweets.each do |tweet| smtp.send_message(content(tweet),@bot['email'],@user['phone']) rescue putd "Error occured sending message:" putd "\tSent: #{tweet.user.screen_name}: #{tweet.text[0..20]}..." end end putd "Messages sent." rescue putd "Error occured starting smtp. Perhaps account info is incorrect" + " or Internet is down?" putd " Message was: #{$!}" end end def receive_messages Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE) # maybe verify... Net::POP3.start('pop.gmail.com',995, @bot['email'], @bot['password']) do |pop| pop.each_mail do |mail| process_pop_message(mail.pop) end end end private # Put Debug (prepend time) if debug printouts turned on def putd (message) if @config['debug'] if @config['log_to'] == 'file' @logger ||= TwitterSms::Logger.new("#{ENV['HOME']}/.twitter_sms-log") @logger.log(message) elsif @config['log_to'] == 'console' puts "#{Time.now.strftime("(%b %d - %H:%M:%S)")} #{message}" end else puts message end end # Verify syntax for dissecting pop message def process_pop_message(msg) TwitterSms::space_stripper!(msg) # Remove sometimes broken extra spaces r_msg = RMail::Parser.read(msg) #must be extracted before we reduce to plaintext from = r_msg.header.from[0].address plain = reduce_to_plaintext(r_msg) body = plain.body if from == @user['phone'] # must come from phone # Extract this log later if body =~ /^off\w*/i @config['active'] = false putd "Received command to disable texting" elsif body =~ /^on\w*/i @config['active'] = true putd "Received command to enable texting" else body.scan(/ignore (\w+)/i) {|_| @config['no_follow'] << $1 } body.scan(/follow (\w+)/i) {|_| @config['no_follow'] -= [$1] } # also set follow end end end # Sometimes Rmail messages are multipart, we only want plaintext def reduce_to_plaintext(r_msg) if r_msg.multipart? r_msg = r_msg.body.find do |part| part.header.content_type == "text/plain" end else r_msg end end # Parse and store a config file (either as an initial load or as # an update) def load_config(config_file=@config_file['name']) loaded_config = YAML.load_file(config_file) @config_file = { 'name' => config_file, 'modified_at' => File.mtime(config_file) } # Seperate config hashes into easier to use parts @user = loaded_config['user'] @bot = loaded_config['bot'] # Extract this to YAML defaults file config_defaults = { 'own_tweets' => false, 'keep_alive' => true, 'per_hour' => 30, 'debug' => false, 'dont_refresh' => false, 'active' => true, 'log_to' => 'file'} # Merge specified config onto defaults @config = config_defaults.merge(loaded_config['config']) set_wait # Manually set "last_check" so update actually pulls prior tweets @last_check = Time.now - @config['wait'] putd "Loaded config file" end # @config['wait'] is the number of seconds to wait def set_wait @config['wait'] = SEC_PER_HOUR / @config['per_hour'] end def load_opts(args) options = OptionParser.new do |opts| opts.banner = "Twitter-sms bot help menu:\n" opts.banner += "==========================\n" opts.banner += "Usage #$0 [options]" # The problem with this is if the asked for config file doesn't exist opts.on('-c', '--config-file [FILE]', 'Location of the config file to be loaded') do |filename| load_config(filename) end opts.on('-t', '--times-per-hour [TIMES]', 'Indicate the amount of times per hour to check (> 0)') do |times| times = 1 if times <= 0 @config['per_hour'] = times end opts.on('-o', '--own-tweets', "Makes your own tweets send in addition to followed account\'s tweets") do @config['own_tweets'] = true end opts.on('-s', '--single-check', 'Forces the program to only check once, instead of continually') do @config['keep_alive'] = false end opts.on_tail('-h', '--help', 'display this help and exit') do puts opts exit end end options.parse!(args) end # Config is stale if a newer version exists in the filesystem than last checked def config_stale? return File.mtime(@config_file['name']) > @config_file['modified_at'] end # Produce the content of the SMTP message to be sent def content(tweet) "From: #{@bot['email']}\n"+ "To: #{@user['phone']}\n"+ #On date line 2 '\n's are required for proper message "Date: #{Time.parse(tweet.created_at).rfc2822}\n\n"+ "#{tweet.user.screen_name}: #{CGI.escapeHTML(tweet.text)}" end def self.filename_to_ary(path) path.split('/')[-1] end end end