Create new ruby gem - Moriarty part-3
decentralizuj
Posted on April 2, 2021
decentralizuj / moriarty
Moriarty - Tool to check social networks for available username
In past series I created moriarti.rb
(responsible for everything). I love to work with ruby scripts, because of ability to write powerful/useful code in short time. But this only works for "small projects". If we want to extend script after some time, It'll become pain in the ass. For this we have ruby-gems, which are easily extendible even for different developer.
- Performance advice:
Each require make ruby slower. Do not create gem with a few lines of code and many requires. For that case, make a script file and use it until you want to extend it.
Create Ruby Gem
First thing is to create directory where you want your ruby gem. In root directory create bin
and lib
folders. That's same for every gem.
# Moriarty directory tree
moriarty |
| - bin/
|
| - lib/
Lib folder handle our scripts, and bin (binaries) handle executable files. Now let's go back to the root of our gem. We need few more files to add there.
# Moriarty root files
Gemfile
Rakefile
README.md # important!
moriarty.gemspec
Let's talk about README file first. It's important to create and to write few lines about gem. Even if you do not plan to opensource it, or share with others, after some time you'll forget what you did 6 month ago in which file. To avoid time and nerve lose, don't be lazy and create it. Same thing with comments in source code - it's not must to go in details, just something to help you remember those details.
Pro TIP:
Too many comments make source code unreadable. make comments short, write code to be readable. I wrote all this comments for purpose of learning. If you want to add comments in right way, learn RDoc. I will write another article about generating RDoc from gem's comments.
Gemfile handle our dependencies from .gemspec
file, so bundler know what to install:
# Moriarty Gemfile
source 'https://rubygems.org'
# this line tell bundler where to look
gemspec
Gemspec file contain everything about our gem. Dependencies, name, description, version, site... but also all files that our gem use. Specification also have data about executable files, so when we install our gem locally, we can run it moriarty USERNAME
instead of ruby bin/moriarty
from root directory.
This is a reason why all objects should define path when work with files, because working directory is not always script directory.
# Gemspec file (moriarty.gemspec)
# frozen_string_literal: true
Gem::Specification.new do |s|
s.name = 'name'
s.version = '0.1.0'
s.summary = 'short description'
s.description = <<~DESC
# Write long description here.
DESC
s.authors = ['...']
s.homepage = '...'
s.license = '...'
s.metadata['homepage_uri'] = '...'
s.metadata['source_code_uri'] = '...'
s.metadata['bug_tracker_uri'] = '...'
# here you add all files from your gem
s.files = ['README.md', 'LICENSE']
# directory with executable
s.bindir = 'bin'
# one or more executable files
s.executables = ['...']
# directory with our files
s.require_paths = ['lib']
# define gems needed to work
s.add_runtime_dependency 'colorize', '~> 0.8.1'
s.add_runtime_dependency 'rest-client', '~> 2.1.0'
s.add_runtime_dependency 'nokogiri', '~> 1.11.2'
# define gems needed for development
s.add_development_dependency 'bundler', '~> 2.2.9'
s.add_development_dependency 'rake', '~> 13.0.3'
end
With all of this, we're ready to create files that will handle our code. In lib/
we will create file moriarty.rb
, and that file will require all other files. This way our executable (or other gem) need to require just one file for everything to work.
# lib/moriarty.rb
#!/usr/bin/env ruby
require 'rest-client'
require 'colorize'
require 'nokogiri'
require_relative 'moriarty/cli'
Here I required dependencies from gemspec. I explained in first article why I use rest-client. I also required relative file moriarti/cli, and we need to create it. Don't think about it too much for now.
From our script file I will copy almost all methods. I will not copy #find!
and code we used to run script. Only code to construct username, url, send request and receive response.
#
# Main class to get data and execute request
#
# Methods: [ #new, #go, #success?, #make_url, #url=, #user= ]
#
# Attributes:
# :user = :moriarty => 'moriarty'
# :url = 'example.com' => 'https://example.com/'
# :response => [.code, .headers, .body] -> restclient#get
# :html => scrapped HTML if success? -> nokogiri#html
#
class Moriarty
attr_reader :url, :user, :response, :html
# Set username and site for search request
# exclude 'https', #make_url will add it for you
# To use different protocol, set it as third parameter
#
# @jim = Moriarty.new( 'moriarty', 'example.com', :http )
# => @jim.user == 'moriarty'
# => @jim.url == 'http://example.com/'
def initialize( name = '', site = 'github.com', type = :https )
@user = name.to_s
@url = make_url site, type
end
# execute request (args are optional)
# @jim.go site: 'github.com', user: 'mynickname'
# => true/false
# -> @jim.response (.code, .headers, .body)
# -> @jim.html (page HTML if request success)
def go( opt = {} )
opt[:user] ||= @user
url = opt[:site].nil? ? @url : make_url(opt[:site])
uri = url + opt[:user]
@response = RestClient.get uri
@html = Nokogiri::HTML @response
return @success = true
rescue
return @success = false
end
alias search go
# create URL from site name, add 'https' if needed
# @jim.make_url 'instagram.com'
# => 'https://instagram.com/'
def make_url( link, prot = :https )
prot = nil if link.to_s.start_with? prot.to_s
url = prot.nil? ? link.to_s : prot.to_s + '://' + link.to_s
url += '/' unless url.end_with?('/')
return url
end
# Set URL from site name and protocol(optional)
# @jim.url = 'github.com'
# => @jim.url == 'https://github.com'
def url=( link, start = :https )
@url = make_url link, start
end
# Set username from string or :symbol
# @jim.user = :moriarty
# => @jim.user == 'moriarty'
def user=( name )
@user = name.to_s
end
# Check does request succeeded or not
# @jim.success?
# => true/false
def success?
@success == true
end
end
Now it's time to create lib/moriarty
directory, and cli.rb
inside. That file I will use for methods we need in CLI (in future versions, that will be removed).
class Moriarty
class << self
# Moriarty.find! 'smartuser'
# => [FOUND!] if username 'smartuser' is free on github
#
# Moriarty.find! :stupiduser, 'facebook.com', :hunt
# => [FREE!] if user 'stupiduser' is registered on facebook
def find!( username, web = 'github.com', type = :search )
@jim = Moriarty.new username.to_s, web.to_s
@jim.go
name = print_name(username).to_s
site = print_url(web).to_s
case
when type.to_sym == :hunt && @jim.success?
p1('+')
p1('FOUND!')
p2(" #{name}", :cyan, :bold)
p2(" found on >> ")
puts p2(site, :cyan, :bold)
when type.to_sym == :hunt && !@jim.success?
p1('-', :red, :red)
p2(" #{name} fail on ", :red)
puts p2(site, :red)
when @jim.success?
p1('-', :red, :red)
p2(" #{name} is taken on ", :red)
puts p2(site, :red)
else
p1('+')
p1('FREE!')
p2(" #{name}", :cyan, :bold)
p2(" is free on >> ")
puts p2(site, :cyan, :bold)
end
end
# #hunt! is alias for #find! with :hunt option
# Check is user 'stupiduser' registered on instagram
# -> Moriarty.hunt! 'stupiduser', 'instagram.com'
def hunt!( name, site = 'github.com', type = :hunt)
find! name, site, type
end
# Remove extensions from domain name
# Moriarty.print_url('https://www.github.com')
# => 'github'
def print_url( site )
site, name = site.to_s, ''
if site.start_with?('http')
site.gsub!("https://", '') or site.gsub!("http://", '')
end
site.gsub!("www.", '') if site.start_with?('www.')
ext = site.to_s.split('.').last
name = site.gsub(".#{ext}", '')
name = name.split('/').first if ext.size < 5
return name.capitalize
end
# Remove extensions from username
# Moriarty.print_name('@moriarty')
# => 'moriarty'
def print_name( name )
name.gsub!('@','') if name.to_s.start_with?('@')
name.gsub!('#','') if name.to_s.start_with?('#')
name.gsub!('/u/','') if name.to_s.start_with?('/u/')
return name
end
def p1( title, color1 = :'light_green', color2 = :cyan, type = :bold )
str = ' ['.colorize(color2) + title.to_s.colorize(color1) + ']'.colorize(color2)
str = str.bold if type == :bold
print str
end
def p2( title, color = :cyan, type = :regular)
str = title.colorize(color)
str = str.bold if type == :bold
print str
end
end
end
In cli.rb
we again define class Moriarty
, but this time accept only self
methods. Note that you can create as many files as you want, just require them in main lib/moriarty.rb
, and add in .gemspec
.
Now we have our classes, but we need a way to execute it in terminal. For that we will create bin/moriarty
:
#!/usr/bin/env ruby
# require 'lib/moriarty', which will require 'moriarti/cli'
require_relative '../lib/moriarty'
# Simple way to try first version
# accept '--hunt' as argument
hunt = ARGV.include?('--hunt') ? :hunt : :search
message = hunt == :hunt ? "Starting hunt on user -> " : "Starting search for free username -> "
# If no args, or include '-h' or '--help', print banner
# If use '--hunt', do not search for '--hunt' username
if ARGV.empty? or ARGV.include? '-h' or ARGV.include? '--help'
puts "\nSearch social networks for free username".cyan
puts " $ ".white + "ruby bin/moriarty sherlock watson".green
puts "\nRun with --hunt to search for registered users".cyan
puts " $ ".white + "ruby bin/moriarty sherlock watson --hunt\n".green
exit(1)
else
system 'clear' or system 'cls'
start_time = Time.now.to_i
ARGV.each do |user|
next if user == '--hunt'
puts
Moriarty.p2(message, :cyan, :bold)
Moriarty.p1(user)
puts; puts
Moriarty.find! user, 'github.com', hunt
Moriarty.find! user, 'rubygems.org/profiles', hunt
Moriarty.find! "@#{user}", 'dev.to', hunt
Moriarty.find! "@#{user}", 'medium.com', hunt
Moriarty.find! "/u/#{user}", 'reddit.com', hunt
Moriarty.find! user, 'youtube.com', hunt
Moriarty.find! user, 'facebook.com', hunt
Moriarty.find! user, 'instagram.com', hunt
Moriarty.find! "@#{user}", 'tiktok.com', hunt
end
end_time = Time.now.to_i
counter = end_time - start_time
sec = ' second'
sec += 's' unless counter.to_s.end_with?('1')
puts
Moriarty.p2('Finished in ->', :cyan, :bold)
Moriarty.p1(counter)
Moriarty.p2(sec, :cyan, :bold)
puts; puts
end
Don't forget to give this file executable permissions:
sudo chmod +x bin/moriarty
Almost done! We need to add all those files in .gemspec
:
s.files = ['bin/moriarty', 'lib/moriarty.rb', 'lib/moriarty/cli.rb', 'moriarty.gemspec',
'README.md', 'LICENSE']
s.bindir = 'bin'
s.executables = ['moriarty']
I added license file here (MIT in my case), that's all up to you. The easiest way is to create repository on GitHub and choose license.
To be able to use rake
tasks, open Rakefile
and add:
require 'bundler/gem_tasks'
Now you can use your gem from terminal:
ruby bin/moriarti sherlock --hunt
Or compile into gem:
bundle exec rake build
To push into rubygems:
# register on rubygems
gem push pkg/moriarti-0.1.0.gem
Or upload to github and use from git:
# Gemfile of some app
require 'moriarty', '~> 0.1.0', git: 'https:github.com/....'
Next article will be about refactoring gem and terminal use (options, colors, output...)
Posted on April 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.