Class: TC::Twitterer

Inherits:
Object
  • Object
show all
Defined in:
lib/tc/twitterer.rb,
lib/tc/twitterer/version.rb

Constant Summary collapse

MAX_TWEET_LENGTH =
140
MAX_URL_LENGTH =
23
MAX_STRING_LENGTH =
MAX_TWEET_LENGTH - MAX_URL_LENGTH
MAX_HISTORY_LENGTH =
MAX_STRING_LENGTH
LOG_LEVEL_MAP =
{
  'debug' => Logger::DEBUG,
  'info'  => Logger::INFO,
  'warn'  => Logger::WARN,
  'error' => Logger::ERROR,
  'fatal' => Logger::FATAL,
}
DEFAULT_LOG_LEVEL =
'warn'
VERSION =
"0.1.1"

Instance Method Summary collapse

Constructor Details

#initialize(config_path, log_level, dry_run) ⇒ Twitterer

Returns a new instance of Twitterer.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 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

Instance Method Details

#fetch_file(repo, hash, path) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# 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) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# 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) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# 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

#runObject



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# 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) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# 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) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
# 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) ⇒ Object



231
232
233
234
235
236
237
238
# 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) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
# 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