Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Created October 25, 2014 22:30
Show Gist options
  • Save ttscoff/dbc549bc9cc2aeb30836 to your computer and use it in GitHub Desktop.
Save ttscoff/dbc549bc9cc2aeb30836 to your computer and use it in GitHub Desktop.
Bitlyize Service/CLI to shorten links and automatically add affiliate tags
#!/usr/bin/ruby
# encoding: utf-8
# Bitlyize by Brett Terpstra 2014
# Shortens all URLs in input and automatically adds Amazon and iTunes affiliate codes
# A single bitly link passed to this script will return the long url
# This script was designed for use in an OS X System Service, but runs
# as a CLI and can be included as a Ruby library in other scripts
#
# The bitly_username and bitly_key variables are required
# Optionally configure the custom domain, and itunes and amazon affiliate variables
#
# MIT License <http://opensource.org/licenses/MIT>
BITLYIZE_VERSION = "1.5.0"
def run!
require 'shellwords'
return false unless __FILE__==$0
### Configuration
# find your bit.ly key at <https://bitly.com/a/settings/advanced>
# under "Legacy API Key"
bitly_username = ''
bitly_key = ''
bl = BitlyLink.new(bitly_username, bitly_key)
# if you have a custom domain, enter it here, otherwise leave blank
bl.bitly_domain = ''
# if you have an iTunes affiliate acount, enter the string
# for `at` and optionally `ct`. Affiliate links are only
# appended to itunes.apple.com urls which don't already contain
# affiliate info
# example: bl.itunes_aff = 'at=10l4tL&ct=bitlyize'
bl.itunes_aff = 'at=10l4tL&ct=bitlyize'
# to create Amazon affiliate links, set amazon_partner to:
# [tag, camp, creative]
# Use the amazon link tool to create any affiliate link and examine
# to find the needed parts. Set to false to return regular amazon links
# example: amazon_aff = ["bretttercom-20","1789","390957"]
bl.amazon_aff = ["brettterpstra-20", "1789", "9325"]
### End Configuration
begin
if ARGV[0] =~ /-?-v(ersion)/
puts "Bitlyize v#{BITLYIZE_VERSION}"
puts "Brett Terpstra, 2014. MIT license."
Process.exit 0
end
clipboard = $clipboard
args = []
while ARGV.length > 0
case ARGV[0]
when /-?-c(opy)?/
clipboard = true
else
args.push(ARGV[0])
end
ARGV.shift
end
if RUBY_VERSION.to_f > 1.9
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8
end
if $debug
input = "https://itunes.apple.com/us/app/datalove-stats-tracker/id699097772"
else
if args.length > 0
input = args.join("\n")
else
input = STDIN.read
end
input = input.force_encoding('utf-8') if input.respond_to? 'force_encoding'
end
unless bitly_username.length > 0 && bitly_key.length > 0
return "ERROR: Username and key are not configured\n\n#{input}"
end
match = input.match(/^(\s*)/)
head = match[1] rescue ''
match = input.match(/(\n++)$/)
tail = match[1] rescue ''
input.strip!
url_re = /(https?:\/\/)?[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&;:\/~\+#]*[\w\-\@^=%&;\/~\+#])?/
link_count = input.scan(url_re).length
shortened_count = 0
input.gsub!(url_re) {|url|
if url =~ /https?:\/\/(bit(\.ly|ly\.com)|#{bl.bitly_domain}|amzn\.to)\//
if link_count == 1
bl.get_long_link(url)
else
url
end
else
short_url = bl.get_short_link(url)
if short_url
shortened_count += 1
short_url
else
url
end
end
}
$stderr.puts "#{link_count} links found, #{shortened_count} shortened.#{clipboard ? " Output in clipboard." : ""}\n\n"
if clipboard
%x{echo #{Shellwords.escape(input.strip)}| sed -e :a -e '/^\\n*$/N;/\\n$/ba' | pbcopy}
else
print head if head
print input
print tail if tail
end
rescue => e
p e.backtrace
end
end
require 'net/http'
require 'net/https'
require 'uri'
require 'cgi'
class BitlyLink
attr_accessor :bitly_domain, :itunes_aff, :amazon_aff
def initialize(username, apikey)
@username = username
@apikey = apikey
@bitly_domain ||= ''
@itunes_aff ||= ''
@amazon_aff ||= ''
end
def get_long_link(shortURL)
begin
res = bitly_call('expand', {'shortURL' => shortURL})
return shortURL unless res
json = JSON.parse(res)
p res if $debug
if json['status_code'] == 200
long = json['data']['expand'][0]['long_url']
else
return shortURL
end
return long
rescue Exception => e
$stderr.puts "Error expanding link"
$stderr.puts e
return shortURL
end
end
def get_short_link(url)
if url
url = affiliatize(url)
else
$stderr.puts "No url provided"
end
begin
res = bitly_call('shorten', {'longUrl' => url, 'domain' => @bitly_domain})
return url unless res
json = JSON.parse(res)
if json['status_code'] == 200
short = json['data']['url']
else
return url
end
return short
rescue Exception => e
$stderr.puts "Error shortening link"
$stderr.puts e
return url
end
end
private
def bitly_call(method, data = {})
begin
path = "/v3/#{method}"
res = nil
http = Net::HTTP.new('api-ssl.bitly.com', 443)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.start do |req|
request = Net::HTTP::Get.new(path+create_query(data))
res = req.request(request).body
end
return false if res.nil?
p res if $debug
return res
rescue Exception => e
p e
return false
end
end
def create_query(data = {})
components = []
data['login'] = @username
data['apiKey'] = @apikey
data.each {|k,v|
components.push("#{k}=#{CGI.escape(v)}")
}
"?" + components.join("&")
end
def affiliatize(url)
if url =~ /^https?:\/\/itunes.apple.com/ && @itunes_aff.length > 0
url.gsub!(/&(at|ct)=[^&]+/,'')
url.gsub!(/\?(at|ct)=[^&]+&?/,'?')
url.sub!(/\?$/,'')
# return url if url =~ /[&\?]at=\S+/
delim = url =~ /\?\S+?=/ ? "&" : "?"
url.strip + delim + @itunes_aff.sub(/^[&\?]/,'')
elsif url =~ /http:\/\/www.amazon.com\/(?:(.*?)\/)?dp\/([^\?]+)/ && @amazon_aff.length == 3
id = $2
"http://www.amazon.com/gp/product/#{id}/ref=as_li_ss_tl?ie=UTF8&camp=#{@amazon_aff[1]}&creative=#{@amazon_aff[2]}&creativeASIN=#{id}&linkCode=as2&tag=#{@amazon_aff[0]}"
else
url
end
end
end
include_json_class =<<-'ENDJSON'
#
## Stupid small pure Ruby JSON parser & generator.
#
# Copyright © 2013 Mislav Marohnić
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the “Software”), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
require 'strscan'
require 'forwardable'
# Usage:
#
# JSON.parse(json_string) => Array/Hash
# JSON.generate(object) => json string
#
# Run tests by executing this file directly. Pipe standard input to the script to have it
# parsed as JSON and to display the result in Ruby.
#
class JSON
def self.parse(data) new(data).parse end
WSP = /\s+/
OBJ = /[{\[]/; HEN = /\}/; AEN = /\]/
COL = /\s*:\s*/; KEY = /\s*,\s*/
NUM = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/
BOL = /true|false/; NUL = /null/
extend Forwardable
attr_reader :scanner
alias_method :s, :scanner
def_delegators :scanner, :scan, :matched
private :s, :scan, :matched
def initialize data
@scanner = StringScanner.new data.to_s
end
def parse
space
object
end
private
def space() scan WSP end
def endkey() scan(KEY) or space end
def object
matched == '{' ? hash : array if scan(OBJ)
end
def value
object or string or
scan(NUL) ? nil :
scan(BOL) ? matched.size == 4:
scan(NUM) ? eval(matched) :
error
end
def hash
obj = {}
space
repeat_until(HEN) { k = string; scan(COL); obj[k] = value; endkey }
obj
end
def array
ary = []
space
repeat_until(AEN) { ary << value; endkey }
ary
end
SPEC = {'b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t"}
UNI = 'u'; CODE = /[a-fA-F0-9]{4}/
STR = /"/; STE = '"'
ESC = '\\'
def string
if scan(STR)
str, esc = '', false
while c = s.getch
if esc
str << (c == UNI ? (s.scan(CODE) || error).to_i(16).chr : SPEC[c] || c)
esc = false
else
case c
when ESC then esc = true
when STE then break
else str << c
end
end
end
str
end
end
def error
raise "parse error at: #{scan(/.{1,10}/m).inspect}"
end
def repeat_until reg
until scan(reg)
pos = s.pos
yield
error unless s.pos > pos
end
end
module Generator
def generate(obj)
raise ArgumentError unless obj.is_a? Array or obj.is_a? Hash
generate_type(obj)
end
alias dump generate
private
def generate_type(obj)
type = obj.is_a?(Numeric) ? :Numeric : obj.class.name
begin send(:"generate_#{type}", obj)
rescue NoMethodError; raise ArgumentError, "can't serialize #{type}"
end
end
ESC_MAP = Hash.new {|h,k| k }.update \
"\r" => 'r',
"\n" => 'n',
"\f" => 'f',
"\t" => 't',
"\b" => 'b'
def quote(str) %("#{str}") end
def generate_String(str)
quote str.gsub(/[\r\n\f\t\b"\\]/) { "\\#{ESC_MAP[$&]}"}
end
def generate_simple(obj) obj.inspect end
alias generate_Numeric generate_simple
alias generate_TrueClass generate_simple
alias generate_FalseClass generate_simple
def generate_Symbol(sym) generate_String(sym.to_s) end
def generate_Time(time)
quote time.strftime(time.utc? ? "%F %T UTC" : "%F %T %z")
end
def generate_Date(date) quote date.to_s end
def generate_NilClass(*) 'null' end
def generate_Array(ary) '[%s]' % ary.map {|o| generate_type(o) }.join(', ') end
def generate_Hash(hash)
'{%s}' % hash.map { |key, value|
"#{generate_String(key.to_s)}: #{generate_type(value)}"
}.join(', ')
end
end
extend Generator
end
ENDJSON
if __FILE__==$0
begin
eval(include_json_class)
$clipboard = false
run!
rescue => e
p e
Process.exit 1
end
else
require 'json'
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment