nomius 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +5 -0
  3. data/.editorconfig +12 -0
  4. data/.overcommit.yml +29 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +27 -0
  7. data/CHANGELOG.md +5 -0
  8. data/CODE_OF_CONDUCT.md +133 -0
  9. data/CONTRIBUTING.md +63 -0
  10. data/Dockerfile +11 -0
  11. data/Gemfile +20 -0
  12. data/LICENSE +21 -0
  13. data/README.md +297 -0
  14. data/Rakefile +12 -0
  15. data/exe/nomius +6 -0
  16. data/lib/nomius/bulk_checker.rb +39 -0
  17. data/lib/nomius/checker.rb +30 -0
  18. data/lib/nomius/cli/command.rb +106 -0
  19. data/lib/nomius/cli/parser/file_parser/csv_parser.rb +31 -0
  20. data/lib/nomius/cli/parser/file_parser/txt_parser.rb +40 -0
  21. data/lib/nomius/cli/parser/file_parser.rb +27 -0
  22. data/lib/nomius/cli/parser/strings_parser.rb +37 -0
  23. data/lib/nomius/cli/parser.rb +22 -0
  24. data/lib/nomius/cli/runner.rb +42 -0
  25. data/lib/nomius/cli/writer/console_writer.rb +68 -0
  26. data/lib/nomius/cli/writer/csv_writer.rb +51 -0
  27. data/lib/nomius/cli.rb +24 -0
  28. data/lib/nomius/detector/base_domain_name_detector.rb +86 -0
  29. data/lib/nomius/detector/base_url_detector.rb +46 -0
  30. data/lib/nomius/detector/dockerhub_detector.rb +26 -0
  31. data/lib/nomius/detector/domain_com_detector.rb +15 -0
  32. data/lib/nomius/detector/domain_org_detector.rb +15 -0
  33. data/lib/nomius/detector/github_detector.rb +26 -0
  34. data/lib/nomius/detector/npmjs_detector.rb +26 -0
  35. data/lib/nomius/detector/pypi_detector.rb +26 -0
  36. data/lib/nomius/detector/rubygems_detector.rb +26 -0
  37. data/lib/nomius/detector/util/http_requester.rb +78 -0
  38. data/lib/nomius/detector.rb +28 -0
  39. data/lib/nomius/logger/silent.rb +31 -0
  40. data/lib/nomius/logger/verbose.rb +47 -0
  41. data/lib/nomius/logger.rb +18 -0
  42. data/lib/nomius/name.rb +26 -0
  43. data/lib/nomius/status/available.rb +22 -0
  44. data/lib/nomius/status/base.rb +16 -0
  45. data/lib/nomius/status/formatter/ascii_mark.rb +24 -0
  46. data/lib/nomius/status/formatter/mark.rb +24 -0
  47. data/lib/nomius/status/unavailable.rb +22 -0
  48. data/lib/nomius/status/unresolved.rb +22 -0
  49. data/lib/nomius/status.rb +5 -0
  50. data/lib/nomius/version.rb +5 -0
  51. data/lib/nomius.rb +13 -0
  52. metadata +229 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "checker"
4
+ require_relative "detector"
5
+ require_relative "name"
6
+ require_relative "logger/silent"
7
+ require_relative "status/formatter/mark"
8
+
9
+ module Nomius
10
+ # BulkChecker
11
+ class BulkChecker
12
+ RESULT = Struct.new(:name, :results, keyword_init: true)
13
+
14
+ attr_reader :names, :detectors, :logger
15
+
16
+ def self.check(*args, **kwargs)
17
+ new(*args, **kwargs).check
18
+ end
19
+
20
+ def initialize(names: [], detectors: Detector.all, logger: Logger::Silent.new)
21
+ @names = names.compact.map { |name| Name.for(name) }
22
+ @detectors = detectors
23
+ @logger = logger
24
+ end
25
+
26
+ def check
27
+ logger.start_batch_processing(names.count)
28
+
29
+ names.map do |name|
30
+ logger.batch_record_processing(name) do
31
+ RESULT.new(
32
+ name: name,
33
+ results: Checker.check(name: name, detectors: detectors, logger: logger)
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "detector"
4
+ require_relative "name"
5
+ require_relative "logger/silent"
6
+
7
+ module Nomius
8
+ # Checker
9
+ class Checker
10
+ attr_reader :name, :detectors, :logger
11
+
12
+ def self.check(*args, **kwargs)
13
+ new(*args, **kwargs).check
14
+ end
15
+
16
+ def initialize(name:, detectors: Detector.all, logger: Logger::Silent.new)
17
+ @name = Name.for(name)
18
+ @detectors = detectors
19
+ @logger = logger
20
+ end
21
+
22
+ def check
23
+ detectors.map do |detector|
24
+ logger.log_detector_status do
25
+ detector.status(name: name, logger: logger)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-option"
4
+ require_relative "../version"
5
+
6
+ module Nomius
7
+ class CLI
8
+ # Console options parser
9
+ class Command
10
+ include TTY::Option
11
+
12
+ usage do
13
+ program "nomius"
14
+ no_command
15
+ desc "Nomius — bulk domain & package name availability checker."
16
+ example <<~DESCRIPTION
17
+ Basic usage
18
+ Check "firstname" and "othername" names.
19
+ $ nomius firstname othername
20
+
21
+ Usage with a TXT file
22
+ $ nomius --input names.txt
23
+ or
24
+ $ cat names.txt | nomius
25
+ or
26
+ $ nomius < names.txt
27
+
28
+ Usage with a CSV file
29
+ $ nomius --input names.csv
30
+
31
+ Usage with a CSV file and output to a CSV file
32
+ $ nomius --input names.csv --output results.csv
33
+ DESCRIPTION
34
+ end
35
+
36
+ flag :help do
37
+ short "-h"
38
+ long "--help"
39
+ desc "Print usage"
40
+ end
41
+
42
+ flag :silent do
43
+ short "-s"
44
+ long "--silent"
45
+ desc "Print less output"
46
+ end
47
+
48
+ flag :version do
49
+ long "--version"
50
+ desc "Print version"
51
+ end
52
+
53
+ option :input do
54
+ short "-i"
55
+ long "--input string"
56
+ desc <<~DESCRIPTION
57
+ Input file. Could be:
58
+ - TXT with each name on a separate line;
59
+ - CSV file with 2 columns: "name","comment" ("comment" is optional).
60
+ DESCRIPTION
61
+ end
62
+
63
+ option :output do
64
+ short "-o"
65
+ long "--output string"
66
+ desc "Output CSV file"
67
+ end
68
+
69
+ argument :names do
70
+ arity zero_or_more
71
+ # Read from STDIN to support piping.
72
+ default -> { $stdin.tty? ? [] : $stdin.each_line.to_a }
73
+ convert :list
74
+ end
75
+
76
+ def run
77
+ check_help!
78
+ check_version!
79
+ check_input!
80
+ end
81
+
82
+ private
83
+
84
+ def check_help!
85
+ return unless params[:help]
86
+
87
+ print help
88
+ exit
89
+ end
90
+
91
+ def check_version!
92
+ return unless params[:version]
93
+
94
+ puts Nomius::VERSION
95
+ exit
96
+ end
97
+
98
+ def check_input!
99
+ return unless params[:names].empty? && params[:input].nil?
100
+
101
+ print help
102
+ exit(1)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require_relative "../../../name"
5
+
6
+ module Nomius
7
+ class CLI
8
+ class Parser
9
+ class FileParser
10
+ # Parser for CSV files
11
+ class CSVParser
12
+ attr_reader :file_name
13
+
14
+ def self.names(file_name:)
15
+ new(file_name: file_name).names
16
+ end
17
+
18
+ def initialize(file_name:)
19
+ @file_name = file_name
20
+ end
21
+
22
+ def names
23
+ CSV.read(file_name, skip_blanks: true, liberal_parsing: true).map do |name, comment|
24
+ Name.new(name: name, comment: comment)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../name"
4
+
5
+ module Nomius
6
+ class CLI
7
+ class Parser
8
+ class FileParser
9
+ # Parser for TXT files
10
+ class TXTParser
11
+ attr_reader :file_name
12
+
13
+ def self.names(file_name:)
14
+ new(file_name: file_name).names
15
+ end
16
+
17
+ def initialize(file_name:)
18
+ @file_name = file_name
19
+ end
20
+
21
+ def names
22
+ cleared_names.map do |name|
23
+ Name.new(name: name)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def cleared_names
30
+ File
31
+ .readlines(file_name)
32
+ .reject { |name| name.strip.empty? }
33
+ .sort
34
+ .uniq
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../name"
4
+ require_relative "file_parser/txt_parser"
5
+ require_relative "file_parser/csv_parser"
6
+
7
+ module Nomius
8
+ class CLI
9
+ class Parser
10
+ # Parser for files
11
+ class FileParser
12
+ FALLBACK_PARSER = TXTParser
13
+
14
+ PARSER_BY_FILE_EXTENSION = {
15
+ ".csv" => CSVParser
16
+ }.freeze
17
+
18
+ def self.names(file_name:, **_kwargs)
19
+ PARSER_BY_FILE_EXTENSION
20
+ .fetch(File.extname(file_name), FALLBACK_PARSER)
21
+ .new(file_name: file_name)
22
+ .names
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../name"
4
+
5
+ module Nomius
6
+ class CLI
7
+ class Parser
8
+ # Parser for array of strings
9
+ class StringsParser
10
+ def self.names(strings:, **_kwargs)
11
+ new(strings: strings).names
12
+ end
13
+
14
+ def initialize(strings:, **_kwargs)
15
+ @strings = strings
16
+ end
17
+
18
+ def names
19
+ cleared_names.map do |name|
20
+ Name.new(name: name)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :strings
27
+
28
+ def cleared_names
29
+ strings
30
+ .reject { |name| name.strip.empty? }
31
+ .sort
32
+ .uniq
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parser/file_parser"
4
+ require_relative "parser/strings_parser"
5
+
6
+ module Nomius
7
+ class CLI
8
+ # ParserChooser
9
+ class Parser
10
+ PARSER_BY_INPUT_FILE_PRESENCE = {
11
+ true => FileParser,
12
+ false => StringsParser
13
+ }.freeze
14
+
15
+ def self.names(file_name: nil, strings: [])
16
+ PARSER_BY_INPUT_FILE_PRESENCE
17
+ .fetch(!file_name.nil?)
18
+ .names(file_name: file_name, strings: strings)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parser"
4
+ require_relative "writer/console_writer"
5
+ require_relative "writer/csv_writer"
6
+ require_relative "../bulk_checker"
7
+ require_relative "../logger"
8
+
9
+ module Nomius
10
+ class CLI
11
+ # CLI Runner
12
+ class Runner
13
+ attr_reader :names, :silent, :input, :output
14
+
15
+ def self.run(*args, **kwargs)
16
+ new(*args, **kwargs).run
17
+ end
18
+
19
+ def initialize(names: [], silent: false, input: nil, output: nil)
20
+ @names = names
21
+ @silent = silent
22
+ @input = input
23
+ @output = output
24
+ end
25
+
26
+ def run
27
+ logger = Logger.for(silent: silent)
28
+ writers = [Writer::ConsoleWriter]
29
+ writers << Writer::CSVWriter if output
30
+
31
+ results = BulkChecker.check(
32
+ names: Parser.names(file_name: input, strings: names),
33
+ logger: logger
34
+ )
35
+
36
+ writers.each do |writer|
37
+ writer.write!(results: results, file_name: output)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-table"
4
+ require_relative "../../status/formatter/mark"
5
+
6
+ module Nomius
7
+ class CLI
8
+ class Writer
9
+ # ConsoleWriter
10
+ class ConsoleWriter
11
+ attr_reader :results
12
+
13
+ TTY_TABLE_WIDTH = {
14
+ "fixed" => 80,
15
+ "flexible" => nil
16
+ }.freeze
17
+
18
+ TTY_TABLE_OPTIONS = {
19
+ padding: [0, 1],
20
+ multiline: true,
21
+ width: TTY_TABLE_WIDTH.fetch(ENV.fetch("TTY_TABLE_WIDTH", "flexible"))
22
+ }.compact.freeze
23
+
24
+ def self.write!(*args, **kwargs)
25
+ new(*args, **kwargs).write!
26
+ end
27
+
28
+ def initialize(results: [], **_kwargs)
29
+ @results = results
30
+ end
31
+
32
+ def write!
33
+ table = TTY::Table.new(headers, rows)
34
+ renderer = TTY::Table::Renderer::Unicode.new(table, TTY_TABLE_OPTIONS)
35
+
36
+ puts renderer.render
37
+ end
38
+
39
+ private
40
+
41
+ def headers
42
+ [
43
+ "Name",
44
+ *results.first.results.map(&:detector).map(&:detector_short_name),
45
+ "Comment"
46
+ ]
47
+ end
48
+
49
+ def rows
50
+ results.map do |result|
51
+ [
52
+ result.name.name,
53
+ *result.results.map { |status| status_mark_cell(status) },
54
+ result.name.comment
55
+ ]
56
+ end
57
+ end
58
+
59
+ def status_mark_cell(status)
60
+ {
61
+ value: Status::Formatter::Mark.for(status),
62
+ alignment: :center
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require_relative "../../status/formatter/ascii_mark"
5
+
6
+ module Nomius
7
+ class CLI
8
+ class Writer
9
+ # CSVWriter
10
+ class CSVWriter
11
+ attr_reader :results, :file_name
12
+
13
+ def self.write!(*args, **kwargs)
14
+ new(*args, **kwargs).write!
15
+ end
16
+
17
+ def initialize(file_name:, results: [], **_kwargs)
18
+ @results = results
19
+ @file_name = file_name
20
+ end
21
+
22
+ def write!
23
+ CSV.open(file_name, "w") do |f|
24
+ f << headers
25
+ rows.each { |row| f << row }
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def headers
32
+ [
33
+ "Name",
34
+ "Comment",
35
+ *results.first.results.map(&:detector).map(&:detector_name)
36
+ ]
37
+ end
38
+
39
+ def rows
40
+ results.map do |result|
41
+ [
42
+ result.name.name,
43
+ result.name.comment,
44
+ *result.results.map { |status| Status::Formatter::ASCIIMark.for(status) }
45
+ ]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/nomius/cli.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cli/command"
4
+ require_relative "cli/runner"
5
+
6
+ module Nomius
7
+ # CLI
8
+ class CLI
9
+ attr_reader :names, :silent, :input, :output
10
+
11
+ def self.run(args = ARGV)
12
+ cmd = Command.new.parse(args)
13
+ cmd.run
14
+ params = cmd.params
15
+
16
+ Runner.run(
17
+ names: params[:names],
18
+ silent: params[:silent],
19
+ input: params[:input],
20
+ output: params[:output]
21
+ )
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "retriable"
5
+ require "whois"
6
+ require "whois-parser"
7
+ require_relative "../status"
8
+
9
+ module Nomius
10
+ class Detector
11
+ # Detects domain is taken.
12
+ # Works in 2 steps:
13
+ # 1. Resolve DNS A record.
14
+ # 2. If there is no A record, check WHOIS record.
15
+ class BaseDomainNameDetector
16
+ # NOTE: OpenDNS servers (https://www.opendns.com/)
17
+ NANE_SERVERS = ["208.67.222.222", "208.67.220.220"].freeze
18
+
19
+ STATUS_RESOLVER = {
20
+ true => Status::Available,
21
+ false => Status::Unavailable
22
+ }.freeze
23
+
24
+ attr_reader :name, :logger
25
+
26
+ def self.status(*args, **kwargs)
27
+ new(*args, **kwargs).status
28
+ end
29
+
30
+ def initialize(name:, logger: Logger::Silent)
31
+ @name = name
32
+ @logger = logger
33
+ end
34
+
35
+ def tld
36
+ ".org"
37
+ end
38
+
39
+ def detector_name
40
+ "#{name.name}#{tld}"
41
+ end
42
+
43
+ def detector_short_name
44
+ tld
45
+ end
46
+
47
+ def status
48
+ status_class.new(name: name, detector: self)
49
+ end
50
+
51
+ private
52
+
53
+ def status_class
54
+ STATUS_RESOLVER.fetch(availabile_by_dns? && availabile_by_whois?)
55
+ rescue Whois::ConnectionError, Whois::ParserError, Timeout::Error => e
56
+ logger.log_error(message: "Can't resolve: #{full_domain_name}", detalis: e.message)
57
+ Status::Unresolved
58
+ end
59
+
60
+ def full_domain_name
61
+ "#{name.name}#{tld}"
62
+ end
63
+
64
+ def availabile_by_dns?
65
+ Resolv::DNS
66
+ .new(nameserver: NANE_SERVERS)
67
+ .getresources(Resolv::DNS::Name.create(full_domain_name), Resolv::DNS::Resource::IN::SOA)
68
+ .empty?
69
+ end
70
+
71
+ def availabile_by_whois?
72
+ Retriable.retriable(
73
+ on: [Whois::ConnectionError, Timeout::Error],
74
+ tries: 6, multiplier: 2, base_interval: 1
75
+ ) do
76
+ Whois.whois(full_domain_name).parser.available?
77
+ end
78
+ rescue Whois::ParserError => e
79
+ # NOTE: Hotfix for whois-parser broken parser for .org.
80
+ return true if e.message.include?("Domain not found")
81
+
82
+ raise e
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../status"
4
+ require_relative "util/http_requester"
5
+ module Nomius
6
+ class Detector
7
+ # Base class for detectors.
8
+ class BaseURLDetector
9
+ STATUS_RESOLVER = {
10
+ Util::HTTPRequester::NotFound => Status::Available,
11
+ Util::HTTPRequester::OK => Status::Unavailable,
12
+ Util::HTTPRequester::Unresolved => Status::Unresolved
13
+ }.freeze
14
+
15
+ attr_reader :name, :logger
16
+
17
+ def self.status(*args, **kwargs)
18
+ new(*args, **kwargs).status
19
+ end
20
+
21
+ def initialize(name:, logger: Logger::Silent, http_requester: Util::HTTPRequester)
22
+ @name = name
23
+ @logger = logger
24
+ @http_requester = http_requester
25
+ end
26
+
27
+ def detector_name
28
+ "Undefined"
29
+ end
30
+
31
+ def detector_short_name
32
+ "Undefined"
33
+ end
34
+
35
+ def status
36
+ STATUS_RESOLVER
37
+ .fetch(http_requester.response_status(uri: uri, logger: logger))
38
+ .new(name: name, detector: self)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :http_requester
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_url_detector"
4
+
5
+ module Nomius
6
+ class Detector
7
+ # Check name availability for https://hub.docker.com/
8
+ class DockerhubDetector < BaseURLDetector
9
+ BASE_URL = "https://hub.docker.com/v2/orgs"
10
+
11
+ def detector_name
12
+ "hub.docker.com"
13
+ end
14
+
15
+ def detector_short_name
16
+ "Docker"
17
+ end
18
+
19
+ private
20
+
21
+ def uri
22
+ "#{BASE_URL}/#{name.name}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "base_domain_name_detector"
5
+
6
+ module Nomius
7
+ class Detector
8
+ # Check .com domain name availability
9
+ class DomainComDetector < BaseDomainNameDetector
10
+ def tld
11
+ ".com"
12
+ end
13
+ end
14
+ end
15
+ end