lolcommits 0.9.2 → 0.9.3.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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