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,55 @@
1
+ require 'base64'
2
+
3
+ module Lolcommits
4
+ module Plugin
5
+ class TermOutput < Base
6
+ def run_postcapture
7
+ if terminal_supported?
8
+ if !runner.vcs_info || runner.vcs_info.repo.empty?
9
+ debug 'repo is empty, skipping term output'
10
+ else
11
+ base64 = Base64.encode64(open(runner.main_image, &:read))
12
+ puts "#{begin_escape}1337;File=inline=1:#{base64};alt=#{runner.message};#{end_escape}\n"
13
+ end
14
+ else
15
+ debug 'Disabled, your terminal is not supported (requires iTerm2)'
16
+ end
17
+ end
18
+
19
+ def self.name
20
+ 'term_output'
21
+ end
22
+
23
+ def self.runner_order
24
+ :postcapture
25
+ end
26
+
27
+ def configure_options!
28
+ if terminal_supported?
29
+ super
30
+ else
31
+ puts "Sorry, your terminal does not support the #{self.class.name} plugin (requires iTerm2)"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # escape sequences for tmux sessions differ
38
+ def begin_escape
39
+ tmux? ? "\033Ptmux;\033\033]" : "\033]"
40
+ end
41
+
42
+ def end_escape
43
+ tmux? ? "\a\033\\" : "\a"
44
+ end
45
+
46
+ def tmux?
47
+ !ENV['TMUX'].nil?
48
+ end
49
+
50
+ def terminal_supported?
51
+ ENV['TERM_PROGRAM'] =~ /iTerm/
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,7 +1,4 @@
1
- # -*- encoding : utf-8 -*-
2
1
  # Adapted and expanded from https://github.com/rwtnorton/moar-lolspeak
3
- # which was largely taken from an old Perl script and is sadly is not
4
- # available via rubygems
5
2
 
6
3
  module Lolspeak
7
4
  LOL_DICTIONARY = {
@@ -94,21 +91,23 @@ module Lolspeak
94
91
  end
95
92
 
96
93
  module Lolcommits
97
- class Tranzlate < Plugin
98
- extend Lolspeak
94
+ module Plugin
95
+ class Tranzlate < Base
96
+ extend Lolspeak
99
97
 
100
- def run_precapture
101
- debug "Commit message before: #{runner.message}"
102
- runner.message = self.class.tranzlate(runner.message)
103
- debug "Commit message after: #{runner.message}"
104
- end
98
+ def run_precapture
99
+ debug "Commit message before: #{runner.message}"
100
+ runner.message = self.class.tranzlate(runner.message)
101
+ debug "Commit message after: #{runner.message}"
102
+ end
105
103
 
106
- def self.name
107
- 'tranzlate'
108
- end
104
+ def self.name
105
+ 'tranzlate'
106
+ end
109
107
 
110
- def self.runner_order
111
- :precapture
108
+ def self.runner_order
109
+ :precapture
110
+ end
112
111
  end
113
112
  end
114
113
  end
@@ -0,0 +1,65 @@
1
+ require 'rest_client'
2
+ require 'base64'
3
+
4
+ module Lolcommits
5
+ module Plugin
6
+ class Uploldz < Base
7
+ attr_accessor :endpoint
8
+
9
+ def initialize(runner)
10
+ super
11
+ options.concat(
12
+ %w(
13
+ endpoint
14
+ optional_key
15
+ optional_http_auth_username
16
+ optional_http_auth_password
17
+ )
18
+ )
19
+ end
20
+
21
+ def run_postcapture
22
+ if !runner.vcs_info || runner.vcs_info.repo.empty?
23
+ puts 'Repo is empty, skipping upload'
24
+ else
25
+ debug "Posting capture to #{configuration['endpoint']}"
26
+ RestClient.post(
27
+ configuration['endpoint'],
28
+ {
29
+ file: File.new(runner.main_image),
30
+ message: runner.message,
31
+ repo: runner.vcs_info.repo,
32
+ author_name: runner.vcs_info.author_name,
33
+ author_email: runner.vcs_info.author_email,
34
+ sha: runner.sha,
35
+ key: configuration['optional_key']
36
+ },
37
+ Authorization: authorization_header
38
+ )
39
+ end
40
+ rescue => e
41
+ log_error(e, "ERROR: RestClient POST FAILED #{e.class} - #{e.message}")
42
+ end
43
+
44
+ def configured?
45
+ !configuration['enabled'].nil? && configuration['endpoint']
46
+ end
47
+
48
+ def authorization_header
49
+ user = configuration['optional_http_auth_username']
50
+ password = configuration['optional_http_auth_password']
51
+ return unless user || password
52
+
53
+ 'Basic ' + Base64.encode64("#{user}:#{password}").chomp
54
+ end
55
+
56
+ def self.name
57
+ 'uploldz'
58
+ end
59
+
60
+ def self.runner_order
61
+ :postcapture
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ module Lolcommits
2
+ class PluginManager
3
+ GEM_NAME_PREFIX = /^lolcommits-plugin-/
4
+
5
+ def initialize
6
+ @plugins = []
7
+ end
8
+
9
+ # @return [Array] find all installed and supported plugins, storing to
10
+ # @plugins Array, and returns this array
11
+ def locate_plugins
12
+ gem_list.each do |gem|
13
+ next if gem.name !~ GEM_NAME_PREFIX
14
+ plugin_name = gem.name.split('-', 2).last
15
+ plugin = GemPlugin.new(plugin_name, gem.name, gem)
16
+
17
+ @plugins << plugin if plugin.supported? && !plugin_located?(plugin)
18
+ end
19
+ @plugins
20
+ end
21
+
22
+ # @return [Hash] A hash with all plugin names (minus the prefix) as
23
+ # keys and Plugin objects as values
24
+ def plugins
25
+ h = {}
26
+ @plugins.each do |plugin|
27
+ h[plugin.name] = plugin
28
+ end
29
+ h
30
+ end
31
+
32
+ # require all plugins
33
+ def load_plugins
34
+ @plugins.map(&:activate!)
35
+ end
36
+
37
+ private
38
+
39
+ def plugin_located?(plugin)
40
+ @plugins.any? { |existing| existing.gem_name == plugin.gem_name }
41
+ end
42
+
43
+ def gem_list
44
+ Gem.refresh
45
+ Gem::Specification.respond_to?(:each) ? Gem::Specification : Gem.source_index.find_name('')
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,3 @@
1
- # -*- encoding : utf-8 -*-
2
1
  require 'lolcommits/platform'
3
2
 
4
3
  module Lolcommits
@@ -33,7 +32,6 @@ module Lolcommits
33
32
 
34
33
  # do native plugins that need to happen before capture
35
34
  plugins_for(:precapture).each do |plugin|
36
- debug "Runner: precapture about to execute #{plugin}"
37
35
  plugin.new(self).execute_precapture
38
36
  end
39
37
 
@@ -51,11 +49,10 @@ module Lolcommits
51
49
  # do native plugins that need to happen immediately after capture and
52
50
  # resize this is effectively the "image processing" phase for now,
53
51
  # reserve just for us and handle manually...?
54
- Lolcommits::Loltext.new(self).execute_postcapture
52
+ Lolcommits::Plugin::Loltext.new(self).execute_postcapture
55
53
 
56
54
  # do native plugins that need to happen after capture
57
55
  plugins_for(:postcapture).each do |plugin|
58
- debug "Runner: postcapture about to execute #{plugin}"
59
56
  plugin.new(self).execute_postcapture
60
57
  end
61
58
 
@@ -69,12 +66,14 @@ module Lolcommits
69
66
  end
70
67
  end
71
68
 
69
+ # TODO: - move these plugin methods to Lolcommits::PluginManager after all
70
+ # plugins get "gemified"
72
71
  def plugins_for(position)
73
72
  self.class.plugins.select { |p| p.runner_order == position }
74
73
  end
75
74
 
76
75
  def self.plugins
77
- Lolcommits::Plugin.subclasses
76
+ Lolcommits::Plugin.constants.map(&Lolcommits::Plugin.method(:const_get)) - [Lolcommits::Plugin::Base]
78
77
  end
79
78
 
80
79
  # the main capture
@@ -0,0 +1,20 @@
1
+ module Lolcommits
2
+ module TestHelpers
3
+ module FakeIO
4
+ # stdout captured and returned
5
+ # stdin mapped to inputs an IO stream seperated with enter key presses
6
+ def fake_io_capture(inputs: [])
7
+ input_stream = "#{inputs.join("\r\n")}\r\n"
8
+ $stdin = StringIO.new(input_stream)
9
+ $stdout = StringIO.new
10
+
11
+ yield
12
+
13
+ $stdout.string
14
+ ensure
15
+ $stdin = STDIN
16
+ $stdout = STDOUT
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ module Lolcommits
2
+ module TestHelpers
3
+ module GitRepo
4
+ def repo
5
+ @repo ||= Git.open(repo_path)
6
+ end
7
+
8
+ def repo_path
9
+ '~/.lolcommits/sample-plugin-test-repo'
10
+ end
11
+
12
+ def repo_exists?
13
+ File.directory?(File.expand_path(repo_path, '.git'))
14
+ end
15
+
16
+ def last_commit
17
+ repo.log.first
18
+ end
19
+
20
+ def setup_repo
21
+ return if repo_exists?
22
+ `git init --quiet #{repo_path}`
23
+ end
24
+
25
+ def commit_repo_with_message(message = 'test message', file_name: 'test.txt', file_content: 'testing')
26
+ setup_repo unless repo_exists?
27
+ `echo '#{file_content}' >> #{repo_path}/#{file_name}`
28
+ `cd #{repo_path} && git add #{file_name}`
29
+ `cd #{repo_path} && git commit -m "#{message}"`
30
+ end
31
+
32
+ def in_repo
33
+ return unless repo_exists?
34
+ Dir.chdir(File.expand_path(repo_path)) do
35
+ yield
36
+ end
37
+ end
38
+
39
+ def teardown_repo
40
+ `rm -rf #{repo_path}`
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,4 +1,3 @@
1
- # -*- encoding : utf-8 -*-
2
1
  module Lolcommits
3
2
  # base class ala plugin.rb
4
3
  class VCSInfo
@@ -1,4 +1,4 @@
1
- # -*- encoding : utf-8 -*-
2
1
  module Lolcommits
3
- VERSION = '0.9.2'.freeze
2
+ VERSION = '0.9.3.pre1'.freeze
3
+ GEM_NAME = 'lolcommits'.freeze
4
4
  end
data/lolcommits.gemspec CHANGED
@@ -1,11 +1,11 @@
1
- # -*- encoding: utf-8 -*-
2
1
  lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'lolcommits/version'
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = 'lolcommits'
6
+ s.name = Lolcommits::GEM_NAME.dup
8
7
  s.version = Lolcommits::VERSION.dup
8
+
9
9
  s.authors = ['Matthew Rothenberg', 'Matthew Hutchinson']
10
10
  s.email = ['mrothenberg@gmail.com', 'matt@hiddenloop.com']
11
11
  s.homepage = 'http://mroth.github.com/lolcommits/'
@@ -1,4 +1,3 @@
1
- # -*- encoding : utf-8 -*-
2
1
  require 'coveralls'
3
2
  Coveralls.wear!
4
3
 
@@ -19,7 +18,7 @@ class LolTest < MiniTest::Test
19
18
  # this will test the permissions but only locally, important before building a gem package!
20
19
  #
21
20
  def test_permissions
22
- impact_perms = File.lstat(Lolcommits::Loltext::DEFAULT_FONT_PATH).mode & 0o777
21
+ impact_perms = File.lstat(Lolcommits::Plugin::Loltext::DEFAULT_FONT_PATH).mode & 0o777
23
22
  imagesnap_perms = File.lstat(File.join(Configuration::LOLCOMMITS_ROOT, 'vendor', 'ext', 'imagesnap', 'imagesnap')).mode & 0o777
24
23
  videosnap_perms = File.lstat(File.join(Configuration::LOLCOMMITS_ROOT, 'vendor', 'ext', 'videosnap', 'videosnap')).mode & 0o777
25
24
  commandcam_perms = File.lstat(File.join(Configuration::LOLCOMMITS_ROOT, 'vendor', 'ext', 'CommandCam', 'CommandCam.exe')).mode & 0o777
data/test/plugins_test.rb CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding : utf-8 -*-
2
1
  require 'coveralls'
3
2
  Coveralls.wear!
4
3
 
@@ -19,7 +18,7 @@ class PluginsTest < MiniTest::Test
19
18
  #
20
19
  def test_tranzlate
21
20
  [['what the hell', '(WH|W)UT TEH HELL'], ['seriously wtf', 'SRSLEH WTF']].each do |normal, lol|
22
- tranzlated = Lolcommits::Tranzlate.tranzlate(normal)
21
+ tranzlated = Lolcommits::Plugin::Tranzlate.tranzlate(normal)
23
22
  assert_match(/^#{lol}/, tranzlated)
24
23
  end
25
24
  end
@@ -28,25 +27,25 @@ class PluginsTest < MiniTest::Test
28
27
  # issue #136, https://github.com/mroth/lolcommits/issues/136
29
28
  def test_lol_twitter_build_tweet
30
29
  long_commit_message = FFaker::Lorem.sentence(500)
31
- plugin = Lolcommits::LolTwitter.new(nil)
30
+ plugin = Lolcommits::Plugin::LolTwitter.new(nil)
32
31
  max_tweet_size = 116
33
32
  suffix = '... #lolcommits'
34
33
 
35
- Lolcommits::LolTwitter.send(:define_method, :max_tweet_size, proc { max_tweet_size })
36
- Lolcommits::LolTwitter.send(:define_method, :configuration, proc { {} })
34
+ Lolcommits::Plugin::LolTwitter.send(:define_method, :max_tweet_size, proc { max_tweet_size })
35
+ Lolcommits::Plugin::LolTwitter.send(:define_method, :configuration, proc { {} })
37
36
  assert_equal "#{long_commit_message[0..(max_tweet_size - suffix.length)]}#{suffix}", plugin.build_tweet(long_commit_message)
38
37
  end
39
38
 
40
39
  def test_lol_twitter_prefix_suffix
41
- plugin = Lolcommits::LolTwitter.new(nil)
42
- Lolcommits::LolTwitter.send(:define_method, :max_tweet_size, proc { 116 })
40
+ plugin = Lolcommits::Plugin::LolTwitter.new(nil)
41
+ Lolcommits::Plugin::LolTwitter.send(:define_method, :max_tweet_size, proc { 116 })
43
42
  assert_match 'commit msg #lolcommits', plugin.build_tweet('commit msg')
44
43
 
45
44
  plugin_config = {
46
45
  'prefix' => '@prefixing!',
47
46
  'suffix' => '#suffixing!'
48
47
  }
49
- Lolcommits::LolTwitter.send(:define_method, :configuration, proc { plugin_config })
48
+ Lolcommits::Plugin::LolTwitter.send(:define_method, :configuration, proc { plugin_config })
50
49
  assert_equal '@prefixing! commit msg #suffixing!', plugin.build_tweet('commit msg')
51
50
  end
52
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lolcommits
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Rothenberg
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-01-04 00:00:00.000000000 Z
12
+ date: 2017-03-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aruba
@@ -327,7 +327,6 @@ files:
327
327
  - features/step_definitions/lolcommits_steps.rb
328
328
  - features/support/env.rb
329
329
  - features/support/path_helpers.rb
330
- - lib/core_ext/class.rb
331
330
  - lib/core_ext/mercurial-ruby/command.rb
332
331
  - lib/core_ext/mercurial-ruby/shell.rb
333
332
  - lib/core_ext/mini_magick/utilities.rb
@@ -350,23 +349,27 @@ files:
350
349
  - lib/lolcommits/cli/process_runner.rb
351
350
  - lib/lolcommits/cli/timelapse_gif.rb
352
351
  - lib/lolcommits/configuration.rb
352
+ - lib/lolcommits/gem_plugin.rb
353
353
  - lib/lolcommits/installation.rb
354
354
  - lib/lolcommits/platform.rb
355
- - lib/lolcommits/plugin.rb
356
- - lib/lolcommits/plugins/dot_com.rb
357
- - lib/lolcommits/plugins/lol_flowdock.rb
358
- - lib/lolcommits/plugins/lol_hipchat.rb
359
- - lib/lolcommits/plugins/lol_protonet.rb
360
- - lib/lolcommits/plugins/lol_slack.rb
361
- - lib/lolcommits/plugins/lol_tumblr.rb
362
- - lib/lolcommits/plugins/lol_twitter.rb
363
- - lib/lolcommits/plugins/lol_yammer.rb
364
- - lib/lolcommits/plugins/lolsrv.rb
365
- - lib/lolcommits/plugins/loltext.rb
366
- - lib/lolcommits/plugins/term_output.rb
367
- - lib/lolcommits/plugins/tranzlate.rb
368
- - lib/lolcommits/plugins/uploldz.rb
355
+ - lib/lolcommits/plugin/base.rb
356
+ - lib/lolcommits/plugin/dot_com.rb
357
+ - lib/lolcommits/plugin/lol_flowdock.rb
358
+ - lib/lolcommits/plugin/lol_hipchat.rb
359
+ - lib/lolcommits/plugin/lol_protonet.rb
360
+ - lib/lolcommits/plugin/lol_slack.rb
361
+ - lib/lolcommits/plugin/lol_tumblr.rb
362
+ - lib/lolcommits/plugin/lol_twitter.rb
363
+ - lib/lolcommits/plugin/lol_yammer.rb
364
+ - lib/lolcommits/plugin/lolsrv.rb
365
+ - lib/lolcommits/plugin/loltext.rb
366
+ - lib/lolcommits/plugin/term_output.rb
367
+ - lib/lolcommits/plugin/tranzlate.rb
368
+ - lib/lolcommits/plugin/uploldz.rb
369
+ - lib/lolcommits/plugin_manager.rb
369
370
  - lib/lolcommits/runner.rb
371
+ - lib/lolcommits/test_helpers/fake_io.rb
372
+ - lib/lolcommits/test_helpers/git_repo.rb
370
373
  - lib/lolcommits/vcs_info.rb
371
374
  - lib/lolcommits/version.rb
372
375
  - lolcommits.gemspec
@@ -395,9 +398,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
395
398
  version: '2.0'
396
399
  required_rubygems_version: !ruby/object:Gem::Requirement
397
400
  requirements:
398
- - - ">="
401
+ - - ">"
399
402
  - !ruby/object:Gem::Version
400
- version: '0'
403
+ version: 1.3.1
401
404
  requirements:
402
405
  - imagemagick
403
406
  - a webcam