class TC::Twitterer

Constants

DEFAULT_LOG_LEVEL
LOG_LEVEL_MAP
MAX_HISTORY_LENGTH
MAX_STRING_LENGTH
MAX_TWEET_LENGTH
MAX_URL_LENGTH
VERSION

Public Class Methods

new( config_path, log_level, dry_run ) click to toggle source
# File lib/tc/twitterer.rb, line 29
def initialize( config_path, log_level, dry_run )
  @log = Logger.new( STDERR )
  self.set_log_level( DEFAULT_LOG_LEVEL )

  @log.info 'Starting up'

  begin
    fail 'config file not specified' unless config_path
    fail 'config file not found'     unless File.file?( config_path )

    @log.info "Loading config from #{ config_path }"
    @config = OpenStruct.new( TOML.load_file( config_path ) )
    @log.info 'Loaded config'

  rescue => e
    @log.fatal "Failed to load config: #{e.message}"
    exit 1
  end

  self.set_log_level( @config[ 'log_level' ] )
  self.set_log_level( log_level )

  # very basic sanity check of the config
  [ 'twitter_consumer_key', 'twitter_consumer_secret', 'twitter_access_token', 'twitter_access_token_secret', 'source' ].each do |key|
    unless @config[ key ]
      @log.fatal "Required key '#{ key } not present in config"
      exit 1
    end
  end

  begin
    @twitter = Twitter::REST::Client.new do |config|
      config.consumer_key        = @config.twitter_consumer_key
      config.consumer_secret     = @config.twitter_consumer_secret
      config.access_token        = @config.twitter_access_token
      config.access_token_secret = @config.twitter_access_token_secret
    end

    @log.info "Connected to twitter as '#{@twitter.user.screen_name}'"

  rescue => e
    @log.fatal "Failed to connect to twitter: #{e.message}"
    exit 1
  end

  # open and import the history if configured
  if @config.history_file
    @log.info( "Loading history from '#{ @config.history_file }'" )
    @history = {}

    begin
      unless File.file?( @config.history_file )
        @log.info 'History not present - creating'
        File.write( @config.history_file, nil )
      end

      CSV.foreach( @config.history_file ) do |csv|
        timestamp, source, line = csv

        @log.debug( "Processing history: #{source}/#{line}/#{timestamp}")

        # apparently ruby doesn't autovivify? Vive la perl!
        @history[source]    ||= {}
        @history[source][line] = timestamp
      end

    rescue => e
      @log.fatal( "Failed to import history from '#{ @config.history_file }': #{ e }" )
      exit 1
    end
  end

  if dry_run
    @log.warn 'Dry run mode: ACTIVATED'
    @dry_run = true
  end
end

Public Instance Methods

fetch_file( repo, hash, path ) click to toggle source
# File lib/tc/twitterer.rb, line 153
def fetch_file( repo, hash, path )
  @log.info "Fetching '#{repo}/#{path}' at '#{hash}'"

  begin
    response = Net::HTTP.get_response( URI( "https://raw.githubusercontent.com/#{repo}/#{hash}/#{path}" ) )

    # this will fail unless we get a 200 OK
    response.value

  rescue => e
    @log.error "Failed to fetch '#{repo}/#{path}' at '#{hash}': #{e}"
    raise e
  end

  @log.debug "Fetched '#{repo}/#{path}' at '#{hash}'"

  response.body
end
pick_line( repo, path, contents ) click to toggle source
# File lib/tc/twitterer.rb, line 172
def pick_line( repo, path, contents )
  n    = 0
  pick = ''
  rows = contents.split( "\n" )

  source = "#{repo}/#{path}"

  @log.info "Picking suitable line from '#{source}'"

  for i in 1..rows.count
    line_number = rand( rows.count )
    line        = rows[ line_number ]

    # must contain an alpha - don't bother logging as this is specified behavior
    next unless line =~ /[a-zA-Z]/

    # mustn't've been used before
    if @config.history_file
      key = line[ 0 .. MAX_HISTORY_LENGTH ]
      if @history.key?( source ) and @history[ source ].key?( key )
        @log.debug "Skipping '#{line}' because we've used it before (#{ @history[ source ][ key ] })"
        next
      end
    end

    # if we're here, we're good to go
    pick = line
    n    = line_number + 1

    break
  end

  if n == 0
    fail "Failed to pick an entry from '#{source}' - exhausted content?"
  end

  @log.debug "Picked '#{pick}' [#{n}] from '#{source}'"

  return pick, n
end
resolve_repo( repo ) click to toggle source
# File lib/tc/twitterer.rb, line 120
def resolve_repo( repo )
  @log.info "Resolving default->hash for '#{repo}'"

  begin
    response = Net::HTTP.get_response( URI( "https://api.github.com/repos/#{repo}" ) )

    # fail if not 200 OK
    response.value

    json = JSON.parse( response.body )

    default_branch = json[ 'default_branch' ]
    @log.info "Found default branch '#{default_branch}'"

    response = Net::HTTP.get_response( URI( "https://api.github.com/repos/#{repo}/git/refs/heads/#{ default_branch }" ) )

    # this will fail unless we get a 200 OK
    response.value

    json = JSON.parse( response.body )

  rescue => e
    @log.error "Failed to resolve '#{repo}' default '#{default_branch}': #{e}"
    raise e
  end

  hash = json['object']['sha']

  @log.debug "Resolved #{default_branch}->#{hash} for '#{repo}'"

  hash
end
run() click to toggle source
# File lib/tc/twitterer.rb, line 252
def run
  @config.source.each do |source|
    @log.info "Processing '#{source}'"

    begin
      if m = source.match( %r{(.*?/.*?)/(.*)} )
        repo, path = m.captures
      else
        fail "Couldn't extract repo and path from '#{source}' - skipping"
        next
      end

      # convert default->hash
      hash = resolve_repo( repo )

      # fetch file
      file_body = fetch_file( repo, hash, path )

      # extract suitable line
      line, line_number = pick_line( repo, path, file_body )

      # generate the tweet and send it
      tweet( repo, hash, path, line, line_number )

      # store in history
      if @config.history_file
        update_history( source, line )
      end

      # all done!
    rescue => e
      @log.error "Failed '#{source}': #{e}"
    end
  end
end
sanitise( line ) click to toggle source
# File lib/tc/twitterer.rb, line 213
def sanitise( line )
  rc = Redcarpet::Markdown.new( Redcarpet::Render::StripDown )

  # remove any markdown
  line = rc.render( line ).strip!

  # compress any whitespace
  line.gsub!( /\s+/, ' ' )

  # truncate to a sane length, add an elipsis if necessary
  line = ( line.length > MAX_STRING_LENGTH ? "#{ line[ 0 .. MAX_STRING_LENGTH ] }..." : line )

  # just in case we truncated after a space
  line.gsub!( /\s\.\.\./, '...' )

  line
end
set_log_level( level ) click to toggle source
# File lib/tc/twitterer.rb, line 107
def set_log_level( level )
  return unless level

  level.downcase!

  unless LOG_LEVEL_MAP[ level ]
    @log.fatal "Unrecognised log_level '#{ level }'"
    exit 1
  end

  @log.level = LOG_LEVEL_MAP[ level ]
end
tweet( repo, hash, path, line, line_number ) click to toggle source
# File lib/tc/twitterer.rb, line 231
def tweet( repo, hash, path, line, line_number )
  link = "https://github.com/#{repo}/blame/#{hash}/#{path}#L#{line_number}"

  tweet = sprintf '%s %s', sanitise( line ), link

  @log.warn sprintf "%sTweeting '%s' [%d] from %s/%s", ( @dry_run == true ? '[DRYRUN] ' : '' ), tweet, tweet.length, repo, path
  @twitter.update( tweet ) unless @dry_run
end
update_history( source, line ) click to toggle source
# File lib/tc/twitterer.rb, line 240
def update_history( source, line )
  # limit the length to avoid bloat in the history file
  line = line[ 0 .. MAX_HISTORY_LENGTH ]

  @log.info sprintf "%sAdding '%s' to history for '%s'", ( @dry_run == true ? '[DRYRUN] ' : '' ), line, source
  return if @dry_run

  CSV.open( @config.history_file, 'a' ) do |csv|
    csv << [ Time.now.strftime( '%FT%T%z' ), source, line ]
  end
end