lolcommits 0.9.2 → 0.9.3.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +6 -0
  3. data/CHANGELOG.md +243 -105
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +10 -3
  6. data/bin/lolcommits +9 -16
  7. data/features/step_definitions/lolcommits_steps.rb +0 -1
  8. data/features/support/env.rb +0 -1
  9. data/features/support/path_helpers.rb +0 -1
  10. data/lib/core_ext/mini_magick/utilities.rb +0 -1
  11. data/lib/lolcommits.rb +16 -16
  12. data/lib/lolcommits/backends/git_info.rb +0 -1
  13. data/lib/lolcommits/backends/installation_git.rb +0 -1
  14. data/lib/lolcommits/backends/installation_mercurial.rb +0 -1
  15. data/lib/lolcommits/backends/mercurial_info.rb +0 -1
  16. data/lib/lolcommits/capturer.rb +0 -1
  17. data/lib/lolcommits/capturer/capture_cygwin.rb +0 -1
  18. data/lib/lolcommits/capturer/capture_fake.rb +0 -1
  19. data/lib/lolcommits/capturer/capture_linux.rb +0 -1
  20. data/lib/lolcommits/capturer/capture_linux_animated.rb +0 -1
  21. data/lib/lolcommits/capturer/capture_mac.rb +0 -1
  22. data/lib/lolcommits/capturer/capture_mac_animated.rb +0 -1
  23. data/lib/lolcommits/capturer/capture_windows.rb +0 -1
  24. data/lib/lolcommits/cli/fatals.rb +0 -8
  25. data/lib/lolcommits/cli/launcher.rb +0 -1
  26. data/lib/lolcommits/cli/process_runner.rb +0 -2
  27. data/lib/lolcommits/cli/timelapse_gif.rb +0 -1
  28. data/lib/lolcommits/configuration.rb +10 -7
  29. data/lib/lolcommits/gem_plugin.rb +46 -0
  30. data/lib/lolcommits/installation.rb +0 -1
  31. data/lib/lolcommits/platform.rb +0 -1
  32. data/lib/lolcommits/plugin/base.rb +110 -0
  33. data/lib/lolcommits/plugin/dot_com.rb +50 -0
  34. data/lib/lolcommits/plugin/lol_flowdock.rb +69 -0
  35. data/lib/lolcommits/plugin/lol_hipchat.rb +124 -0
  36. data/lib/lolcommits/plugin/lol_protonet.rb +68 -0
  37. data/lib/lolcommits/plugin/lol_slack.rb +68 -0
  38. data/lib/lolcommits/plugin/lol_tumblr.rb +129 -0
  39. data/lib/lolcommits/plugin/lol_twitter.rb +176 -0
  40. data/lib/lolcommits/plugin/lol_yammer.rb +84 -0
  41. data/lib/lolcommits/plugin/lolsrv.rb +58 -0
  42. data/lib/lolcommits/plugin/loltext.rb +190 -0
  43. data/lib/lolcommits/plugin/term_output.rb +55 -0
  44. data/lib/lolcommits/{plugins → plugin}/tranzlate.rb +14 -15
  45. data/lib/lolcommits/plugin/uploldz.rb +65 -0
  46. data/lib/lolcommits/plugin_manager.rb +48 -0
  47. data/lib/lolcommits/runner.rb +4 -5
  48. data/lib/lolcommits/test_helpers/fake_io.rb +20 -0
  49. data/lib/lolcommits/test_helpers/git_repo.rb +44 -0
  50. data/lib/lolcommits/vcs_info.rb +0 -1
  51. data/lib/lolcommits/version.rb +2 -2
  52. data/lolcommits.gemspec +2 -2
  53. data/test/lolcommits_test.rb +1 -2
  54. data/test/plugins_test.rb +7 -8
  55. metadata +22 -19
  56. data/lib/core_ext/class.rb +0 -8
  57. data/lib/lolcommits/plugin.rb +0 -123
  58. data/lib/lolcommits/plugins/dot_com.rb +0 -51
  59. data/lib/lolcommits/plugins/lol_flowdock.rb +0 -70
  60. data/lib/lolcommits/plugins/lol_hipchat.rb +0 -125
  61. data/lib/lolcommits/plugins/lol_protonet.rb +0 -69
  62. data/lib/lolcommits/plugins/lol_slack.rb +0 -69
  63. data/lib/lolcommits/plugins/lol_tumblr.rb +0 -129
  64. data/lib/lolcommits/plugins/lol_twitter.rb +0 -176
  65. data/lib/lolcommits/plugins/lol_yammer.rb +0 -85
  66. data/lib/lolcommits/plugins/lolsrv.rb +0 -58
  67. data/lib/lolcommits/plugins/loltext.rb +0 -184
  68. data/lib/lolcommits/plugins/term_output.rb +0 -54
  69. data/lib/lolcommits/plugins/uploldz.rb +0 -66
@@ -0,0 +1,176 @@
1
+ require 'yaml'
2
+ require 'oauth'
3
+ require 'simple_oauth'
4
+ require 'rest_client'
5
+ require 'addressable/uri'
6
+
7
+ module Lolcommits
8
+ module Plugin
9
+ class LolTwitter < Base
10
+ TWITTER_API_ENDPOINT = 'https://api.twitter.com'.freeze
11
+ TWITTER_CONSUMER_KEY = 'qc096dJJCxIiqDNUqEsqQ'.freeze
12
+ TWITTER_CONSUMER_SECRET = 'rvjNdtwSr1H0TvBvjpk6c4bvrNydHmmbvv7gXZQI'.freeze
13
+ TWITTER_RESERVED_MEDIA_CHARS = 24
14
+ TWITTER_RETRIES = 2
15
+ TWITTER_PIN_REGEX = /^\d{4,}$/ # 4 or more digits
16
+ DEFAULT_SUFFIX = '#lolcommits'.freeze
17
+
18
+ def run_postcapture
19
+ tweet = build_tweet(runner.message)
20
+
21
+ attempts = 0
22
+ begin
23
+ attempts += 1
24
+ puts "Tweeting: #{tweet}"
25
+ debug "--> Tweeting! (attempt: #{attempts}, tweet length: #{tweet.length} chars)"
26
+ post_tweet(tweet, File.open(runner.main_image, 'r'))
27
+ rescue StandardError => e
28
+ debug "Tweet FAILED! #{e.class} - #{e.message}"
29
+ retry if attempts < TWITTER_RETRIES
30
+ puts "ERROR: Tweet FAILED! (after #{attempts} attempts) - #{e.message}"
31
+ end
32
+ end
33
+
34
+ def post_url
35
+ # TODO: this endpoint is deprecated, use the new approach instead
36
+ # https://dev.twitter.com/rest/reference/post/statuses/update_with_mediath_media
37
+ @post_url ||= TWITTER_API_ENDPOINT + '/1.1/statuses/update_with_media.json'
38
+ end
39
+
40
+ def post_tweet(status, media)
41
+ RestClient.post(
42
+ post_url,
43
+ {
44
+ 'media[]' => media,
45
+ 'status' => status
46
+ }, Authorization: oauth_header
47
+ )
48
+ end
49
+
50
+ def build_tweet(commit_message)
51
+ prefix = config_with_default('prefix', '')
52
+ suffix = " #{config_with_default('suffix', DEFAULT_SUFFIX)}"
53
+ prefix = "#{prefix} " unless prefix.empty?
54
+
55
+ available_commit_msg_size = max_tweet_size - (prefix.length + suffix.length)
56
+ if commit_message.length > available_commit_msg_size
57
+ commit_message = "#{commit_message[0..(available_commit_msg_size - 3)]}..."
58
+ end
59
+
60
+ "#{prefix}#{commit_message}#{suffix}"
61
+ end
62
+
63
+ def configure_options!
64
+ options = super
65
+ # ask user to configure tokens if enabling
66
+ if options['enabled']
67
+ auth_config = configure_auth!
68
+ return unless auth_config
69
+ options = options.merge(auth_config).merge(configure_prefix_suffix)
70
+ end
71
+ options
72
+ end
73
+
74
+ def configure_auth!
75
+ puts '---------------------------'
76
+ puts 'Need to grab twitter tokens'
77
+ puts '---------------------------'
78
+
79
+ request_token = oauth_consumer.get_request_token
80
+ rtoken = request_token.token
81
+ rsecret = request_token.secret
82
+
83
+ print "\n1) Please open this url in your browser to get a PIN for lolcommits:\n\n"
84
+ puts request_token.authorize_url
85
+ print "\n2) Enter PIN, then press enter: "
86
+ twitter_pin = gets.strip.downcase.to_s
87
+
88
+ unless twitter_pin =~ TWITTER_PIN_REGEX
89
+ puts "\nERROR: '#{twitter_pin}' is not a valid Twitter Auth PIN"
90
+ return
91
+ end
92
+
93
+ begin
94
+ debug "Requesting Twitter OAuth Token with PIN: #{twitter_pin}"
95
+ OAuth::RequestToken.new(oauth_consumer, rtoken, rsecret)
96
+ access_token = request_token.get_access_token(oauth_verifier: twitter_pin)
97
+ rescue OAuth::Unauthorized
98
+ puts "\nERROR: Twitter PIN Auth FAILED!"
99
+ return
100
+ end
101
+
102
+ return unless access_token.token && access_token.secret
103
+ puts ''
104
+ puts '------------------------------'
105
+ puts 'Thanks! Twitter Auth Succeeded'
106
+ puts '------------------------------'
107
+ {
108
+ 'access_token' => access_token.token,
109
+ 'secret' => access_token.secret
110
+ }
111
+ end
112
+
113
+ def configure_prefix_suffix
114
+ print "\n3) Prefix all tweets with something? e.g. @user (leave blank for no prefix): "
115
+ prefix = gets.strip
116
+ print "\n4) End all tweets with something? e.g. #hashtag (leave blank for default suffix #{DEFAULT_SUFFIX}): "
117
+ suffix = gets.strip
118
+
119
+ config = {}
120
+ config['prefix'] = prefix unless prefix.empty?
121
+ config['suffix'] = suffix unless suffix.empty?
122
+ config
123
+ end
124
+
125
+ def configured?
126
+ !configuration['enabled'].nil? &&
127
+ configuration['access_token'] &&
128
+ configuration['secret']
129
+ end
130
+
131
+ def oauth_header
132
+ uri = Addressable::URI.parse(post_url)
133
+ SimpleOAuth::Header.new(:post, uri, {}, oauth_credentials)
134
+ end
135
+
136
+ def oauth_credentials
137
+ {
138
+ consumer_key: TWITTER_CONSUMER_KEY,
139
+ consumer_secret: TWITTER_CONSUMER_SECRET,
140
+ token: configuration['access_token'],
141
+ token_secret: configuration['secret']
142
+ }
143
+ end
144
+
145
+ def oauth_consumer
146
+ @oauth_consumer ||= OAuth::Consumer.new(
147
+ TWITTER_CONSUMER_KEY,
148
+ TWITTER_CONSUMER_SECRET,
149
+ site: TWITTER_API_ENDPOINT,
150
+ request_endpoint: TWITTER_API_ENDPOINT,
151
+ sign_in: true
152
+ )
153
+ end
154
+
155
+ def config_with_default(key, default = nil)
156
+ if configuration[key]
157
+ configuration[key].strip.empty? ? default : configuration[key]
158
+ else
159
+ default
160
+ end
161
+ end
162
+
163
+ def max_tweet_size
164
+ 139 - TWITTER_RESERVED_MEDIA_CHARS
165
+ end
166
+
167
+ def self.name
168
+ 'twitter'
169
+ end
170
+
171
+ def self.runner_order
172
+ :postcapture
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,84 @@
1
+ require 'yammer'
2
+ require 'rest_client'
3
+
4
+ # https://developer.yammer.com/oauth2-quickstart/
5
+ YAMMER_CLIENT_ID = 'bgORyeKtnjZJSMwp8oln9g'.freeze
6
+ YAMMER_CLIENT_SECRET = 'oer2WdGzh74a5QBbW3INUxblHK3yg9KvCZmiBa2r0'.freeze
7
+ YAMMER_ACCESS_TOKEN_URL = 'https://www.yammer.com/oauth2/access_token.json'.freeze
8
+ YAMMER_RETRY_COUNT = 2
9
+
10
+ module Lolcommits
11
+ module Plugin
12
+ class LolYammer < Base
13
+ def self.name
14
+ 'yammer'
15
+ end
16
+
17
+ def self.runner_order
18
+ :postcapture
19
+ end
20
+
21
+ def configured?
22
+ !configuration['access_token'].nil?
23
+ end
24
+
25
+ def configure_access_token
26
+ print "Open the URL below and copy the `code` param from query after redirected, enter it as `access_token`:\n"
27
+ print "https://www.yammer.com/dialog/oauth?client_id=#{YAMMER_CLIENT_ID}&response_type=code\n"
28
+ print 'Enter code param from the redirected URL, then press enter: '
29
+ code = gets.to_s.strip
30
+
31
+ url = YAMMER_ACCESS_TOKEN_URL
32
+ debug "access_token url: #{url}"
33
+ params = {
34
+ 'client_id' => YAMMER_CLIENT_ID,
35
+ 'client_secret' => YAMMER_CLIENT_SECRET,
36
+ 'code' => code
37
+ }
38
+ debug "params : #{params.inspect}"
39
+ result = JSON.parse(RestClient.post(url, params))
40
+ debug "response : #{result.inspect}"
41
+ # no need for 'return', last line is always the return value
42
+ { 'access_token' => result['access_token']['token'] }
43
+ end
44
+
45
+ def configure_options!
46
+ options = super
47
+ # ask user to configure tokens if enabling
48
+ if options['enabled']
49
+ auth_config = configure_access_token
50
+ return unless auth_config
51
+ options.merge!(auth_config)
52
+ end
53
+ options
54
+ end
55
+
56
+ def run_postcapture
57
+ commit_msg = runner.message
58
+ post = "#{commit_msg} #lolcommits"
59
+ puts "Yammer post: #{post}" unless runner.capture_stealth
60
+
61
+ Yammer.configure do |c|
62
+ c.client_id = YAMMER_CLIENT_ID
63
+ c.client_secret = YAMMER_CLIENT_SECRET
64
+ end
65
+
66
+ client = Yammer::Client.new(access_token: configuration['access_token'])
67
+
68
+ retries = YAMMER_RETRY_COUNT
69
+ begin
70
+ lolimage = File.new(runner.main_image)
71
+ response = client.create_message(post, attachment1: lolimage)
72
+ debug response.body.inspect
73
+ puts "\t--> Status posted!" if response
74
+ rescue => e
75
+ retries -= 1
76
+ retry if retries > 0
77
+ puts "Status not posted - #{e.message}"
78
+ puts 'Try running config again:'
79
+ puts "\tlolcommits --config --plugin yammer"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,58 @@
1
+ require 'rest_client'
2
+ require 'pp'
3
+ require 'json'
4
+
5
+ module Lolcommits
6
+ module Plugin
7
+ class Lolsrv < Base
8
+ def initialize(runner)
9
+ super
10
+ options << 'server'
11
+ end
12
+
13
+ def run_postcapture
14
+ fork { sync }
15
+ end
16
+
17
+ def configured?
18
+ !configuration['enabled'].nil? && configuration['server']
19
+ end
20
+
21
+ def sync
22
+ existing = existing_lols
23
+ return unless existing.nil?
24
+ Dir[runner.config.loldir + '/*.{jpg,gif}'].each do |item|
25
+ sha = File.basename(item, '.*')
26
+ upload(item, sha) unless existing.include?(sha) || sha == 'tmp_snapshot'
27
+ end
28
+ end
29
+
30
+ def existing_lols
31
+ lols = JSON.parse(RestClient.get(configuration['server'] + '/lols'))
32
+ lols.map { |lol| lol['sha'] }
33
+ rescue => e
34
+ log_error(e, "ERROR: existing lols could not be retrieved #{e.class} - #{e.message}")
35
+ return nil
36
+ end
37
+
38
+ def upload(file, sha)
39
+ RestClient.post(configuration['server'] + '/uplol',
40
+ lol: File.new(file),
41
+ url: runner.vcs_info.url + sha,
42
+ repo: runner.vcs_info.repo,
43
+ date: File.ctime(file),
44
+ sha: sha)
45
+ rescue => e
46
+ log_error(e, "ERROR: Upload of lol #{sha} FAILED #{e.class} - #{e.message}")
47
+ end
48
+
49
+ def self.name
50
+ 'lolsrv'
51
+ end
52
+
53
+ def self.runner_order
54
+ :postcapture
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,190 @@
1
+ module Lolcommits
2
+ module Plugin
3
+ class Loltext < Base
4
+ DEFAULT_FONT_PATH = File.join(Configuration::LOLCOMMITS_ROOT, 'vendor', 'fonts', 'Impact.ttf')
5
+
6
+ def self.name
7
+ 'loltext'
8
+ end
9
+
10
+ # enabled by default (if no configuration exists)
11
+ def enabled?
12
+ !configured? || super
13
+ end
14
+
15
+ # valid by default (if no config exists)
16
+ def valid_configuration?
17
+ !configured? || super
18
+ end
19
+
20
+ def run_postcapture
21
+ debug 'Annotating image via MiniMagick'
22
+ image = MiniMagick::Image.open(runner.main_image)
23
+ if config_option(:overlay, :enabled)
24
+ image.combine_options do |c|
25
+ c.fill config_option(:overlay, :overlay_colors).sample
26
+ c.colorize config_option(:overlay, :overlay_percent)
27
+ end
28
+ end
29
+
30
+ annotate(image, :message, clean_msg(runner.message))
31
+ annotate(image, :sha, runner.sha)
32
+ debug "Writing changed file to #{runner.main_image}"
33
+ image.write runner.main_image
34
+ end
35
+
36
+ def annotate(image, type, string)
37
+ debug("annotating #{type} to image with #{string}")
38
+
39
+ transformed_position = position_transform(config_option(type, :position))
40
+ annotate_location = '0'
41
+ if transformed_position == 'South'
42
+ annotate_location = '+0+20' # Move South gravity off the edge of the image.
43
+ end
44
+
45
+ string.upcase! if config_option(type, :uppercase)
46
+
47
+ image.combine_options do |c|
48
+ c.strokewidth runner.capture_animated? ? '1' : '2'
49
+ c.interline_spacing(-(config_option(type, :size) / 5))
50
+ c.stroke config_option(type, :stroke_color)
51
+ c.fill config_option(type, :color)
52
+ c.gravity transformed_position
53
+ c.pointsize runner.capture_animated? ? (config_option(type, :size) / 2) : config_option(type, :size)
54
+ c.font config_option(type, :font)
55
+ c.annotate annotate_location, string
56
+ end
57
+ end
58
+
59
+ def configure_options!
60
+ options = super
61
+ # ask user to configure text options when enabling
62
+ if options['enabled']
63
+ puts '---------------------------------------------------------------'
64
+ puts ' LolText options '
65
+ puts ''
66
+ puts ' * any blank options will use the (default)'
67
+ puts ' * always use the full absolute path to fonts'
68
+ puts ' * valid text positions are NE, NW, SE, SW, S, C (centered)'
69
+ puts ' * colors can be hex #FC0 value or a string \'white\''
70
+ puts ' - use `none` for no stroke color'
71
+ puts ' * overlay fills your image with a random color'
72
+ puts ' - set one or more overlay_colors with a comma seperator'
73
+ puts ' - overlay_percent (0-100) sets the fill colorize strength'
74
+ puts '---------------------------------------------------------------'
75
+
76
+ options[:message] = configure_sub_options(:message)
77
+ options[:sha] = configure_sub_options(:sha)
78
+ options[:overlay] = configure_sub_options(:overlay)
79
+ end
80
+ options
81
+ end
82
+
83
+ # TODO: consider this type of configuration prompting in the base Plugin
84
+ # class, working with hash of defaults
85
+ def configure_sub_options(type)
86
+ print "#{type}:\n"
87
+ defaults = config_defaults[type]
88
+
89
+ # sort option keys since different `Hash#keys` varys across Ruby versions
90
+ defaults.keys.sort_by(&:to_s).reduce({}) do |acc, opt|
91
+ # if we have an enabled key set to false, abort asking for any more options
92
+ if acc.key?(:enabled) && acc[:enabled] != true
93
+ acc
94
+ else
95
+ print " #{opt.to_s.tr('_', ' ')} (#{defaults[opt]}): "
96
+ val = parse_user_input(gets.chomp.strip)
97
+ # handle array options (comma seperated string)
98
+ if defaults[opt].is_a?(Array) && !val.nil?
99
+ val = val.split(',').map(&:strip).delete_if(&:empty?)
100
+ end
101
+ acc.merge(opt => val)
102
+ end
103
+ end
104
+ end
105
+
106
+ def config_defaults
107
+ {
108
+ message: {
109
+ color: 'white',
110
+ font: DEFAULT_FONT_PATH,
111
+ position: 'SW',
112
+ size: 48,
113
+ stroke_color: 'black',
114
+ uppercase: false
115
+ },
116
+ sha: {
117
+ color: 'white',
118
+ font: DEFAULT_FONT_PATH,
119
+ position: 'NE',
120
+ size: 32,
121
+ stroke_color: 'black',
122
+ uppercase: false
123
+ },
124
+ overlay: {
125
+ enabled: false,
126
+ overlay_colors: [
127
+ '#2e4970', '#674685', '#ca242f', '#1e7882', '#2884ae', '#4ba000',
128
+ '#187296', '#7e231f', '#017d9f', '#e52d7b', '#0f5eaa', '#e40087',
129
+ '#5566ac', '#ed8833', '#f8991c', '#408c93', '#ba9109'
130
+ ],
131
+ overlay_percent: 50
132
+ }
133
+ }
134
+ end
135
+
136
+ def config_option(type, option)
137
+ default_option = config_defaults[type][option]
138
+ if configuration[type]
139
+ configuration[type][option] || default_option
140
+ else
141
+ default_option
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ # explode psuedo-names for text position
148
+ def position_transform(position)
149
+ case position
150
+ when 'NE'
151
+ 'NorthEast'
152
+ when 'NW'
153
+ 'NorthWest'
154
+ when 'SE'
155
+ 'SouthEast'
156
+ when 'SW'
157
+ 'SouthWest'
158
+ when 'C'
159
+ 'Center'
160
+ when 'S'
161
+ 'South'
162
+ end
163
+ end
164
+
165
+ # do whatever is required to commit message to get it clean and ready for imagemagick
166
+ def clean_msg(text)
167
+ wrapped_text = word_wrap text
168
+ escape_quotes wrapped_text
169
+ escape_ats wrapped_text
170
+ end
171
+
172
+ # conversion for quotation marks to avoid shell interpretation
173
+ # does not seem to be a safe way to escape cross-platform?
174
+ def escape_quotes(text)
175
+ text.gsub(/"/, "''")
176
+ end
177
+
178
+ def escape_ats(text)
179
+ text.gsub(/@/, '\@')
180
+ end
181
+
182
+ # convenience method for word wrapping
183
+ # based on https://github.com/cmdrkeene/memegen/blob/master/lib/meme_generator.rb
184
+ def word_wrap(text, col = 27)
185
+ wrapped = text.gsub(/(.{1,#{col + 4}})(\s+|\Z)/, "\\1\n")
186
+ wrapped.chomp!
187
+ end
188
+ end
189
+ end
190
+ end