nomius 0.1.0

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 (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