Skip to content

Instantly share code, notes, and snippets.

@gyng
Last active December 21, 2015 19:09
Show Gist options
  • Save gyng/6352421 to your computer and use it in GitHub Desktop.
Save gyng/6352421 to your computer and use it in GitHub Desktop.
shipping EXPERT
require 'rubygems'
require 'nokogiri'
require 'open-uri'
class ShipmentTrackerPlugin < Plugin
class ShipmentStatusRecord < Struct.new(:label, :status)
def to_s
if self.status
"\00303#{label}\003: #{status.ircify.gsub("\n", " ")}"
else
"\00303#{label}\003: No information available."
end
end
end
def initialize
super
labels = @registry['labels']
if labels.nil?
@registry['labels'] = []
end
end # initialize
private
def mangle_label_name(label)
return "label::%s" % label
end
def get_scraper_manager
@bot.plugins[:shipmenttrackingutility].scrapers
end
def status_fetch(label)
info = @registry[mangle_label_name(label)]
return ShipmentStatusRecord.new(label, get_scraper_manager.fetch(info[:courier], info[:number]))
end # status_fetch
public
def status_all(m, params)
announce_labels = @registry['labels'].find_all {|label|
th = @registry[mangle_label_name(label)]
if th.nil?
@registry['labels'] = @registry['labels'].find_all {|x| label != x }
false
else
!th.has_key?(:channels) or th[:channels].include?(m.channel.to_s)
end
}
if 0 == announce_labels.size
m.reply "No known items"
end
announce_labels.each {|label|
data = @registry[mangle_label_name(label)]
if not get_scraper_manager.has_courier?(data[:courier])
m.reply "#{label}: Sorry, that courier service is not supported. :("
end
ssr = status_fetch(label)
if ssr
m.reply ssr
else # status = nil
m.reply "#{label}: Sorry, no information is available."
end
}
rescue Exception => e
m.reply e.class
m.reply e
end # status
def status_unnamed(m, params)
number = params[:number]
courier = params[:courier].downcase
if get_scraper_manager.has_courier?(courier)
status = get_scraper_manager.fetch(courier, number)
if status
m.reply status.ircify
else # status = nil
m.reply "Sorry, no information is available."
end # status
else
m.reply "Sorry, that courier service is not supported. :("
end
rescue Exception => e
m.reply e.class
m.reply e
end # status_unnamed
def status_named(m, params)
label = params[:label]
if @registry[mangle_label_name(label)].nil?
m.reply "Sorry, I don't know a shipment by that name"
else
trinfo = @registry[mangle_label_name(label)]
if not get_scraper_manager.has_courier?(trinfo[:courier])
m.reply "Sorry, that courier service is not supported. :("
return
end
status = status_fetch(label)
if status
m.reply status
else # status = nil
m.reply "Sorry, no information is available."
end # status
end
rescue Exception => e
m.reply e.class
m.reply e
if m.channel == '#lolinano'
m.reply e.backtrace
end
end
def cron_notify(m, params)
@tracking_numbers.keys.each {|label|
channel_names = @tracking_numbers[label].has_key?(:channels) and @tracking_numbers[label][:channels]
owner = @tracking_numbers[label].has_key?(:owner) and @tracking_numbers[label][:owner]
msg = status_fetch(label)
next if msg.status.ircify == @registry[msg.label]
if channel_names
for channel_name in channel_names
channel = @bot.channels.find {|ch| ch.name == channel_name }
if owner and channel.users.find {|u| u.nick == owner }
@bot.say channel, [owner, @bot.config['core.nick_postfix'], msg].join
else
@bot.say channel, msg
end
end
else
@bot.say '#', msg
end
@registry[msg.label] = msg.status.ircify
}
end # cron_notify
def show_labels(m, params)
m.reply "Available labels: " + @registry['labels'].\
find_all {|k| @registry[mangle_label_name(k)] }.\
map {|k| "\0033#{k}\017" }.join(', ')
end
def show_couriers
get_scraper_manager.loaded_modules.join(', ')
end
def help(plugin, topic="")
"shipment [ list | add \002Label\017 \002TrackingNumber\017 \002CourierName\017 | del \002Label\017 | \002Label\017 | \002TrackingNumber\017 \002CourierName\017 ]"
end
def add_shipment(m, params)
begin
if not params[:label].is_a?(String)
m.reply "error: wtf label"
return
end
metalabel = mangle_label_name(params[:label])
if !@registry[metalabel].nil?
m.reply "label already exists."
return
end
@registry[metalabel] = {
:number => params[:number],
:courier => params[:courier].to_sym()
}
@registry['labels'] = @registry['labels'] + [params[:label]]
m.reply "Added"
rescue Exception => e
m.reply "I dun goofed. %s, %s" % [e.class, e]
end
end
def del_shipment(m, params)
begin
if not params[:label].is_a?(String)
m.reply "error: wtf label"
return
end
metalabel = mangle_label_name(params[:label])
@registry.delete(metalabel)
@registry['labels'] = @registry['labels'].find_all {|label| label != params[:label] }
m.okay
rescue Exception => e
m.reply "I dun goofed. %s, %s" % [e.class, e]
end
end
end # ShipmentTrackerPlugin
plugin = ShipmentTrackerPlugin.new
plugin.default_auth('notify', false)
plugin.map 'shipment', :action => 'status_all'
plugin.map 'shipment list', :action => 'show_labels'
plugin.map 'shipment cron_notify', :action => 'cron_notify', :auth_path => 'notify'
plugin.map 'shipment del :label', :action => 'del_shipment'
plugin.map 'shipment add :label :number :courier', :action => 'add_shipment'
plugin.map 'shipment :couriers', :action => 'show_couriers'
plugin.map 'shipment :label', :action => 'status_named'
plugin.map 'shipment :number :courier', :action => 'status_unnamed'
require 'rubygems'
require 'nokogiri'
require 'open-uri'
require 'json'
class ShipmentTrackingUtilityPlugin < Plugin
class ShipmentStatus
@number = nil
@location = nil
@time = nil
@activity = nil
@carrier = nil
attr_reader :number, :location, :time, :activity, :carrier
def initialize(data)
@number = data[:number]
@location = data[:location]
@time = data[:time]
@activity = data[:activity]
@carrier = data[:carrier]
end # initialize
def ircify
"#{activity}#{" @ #{time}" if time}#{" - #{location}" if location}"
end
end # ShipmentStatus
module Scrapers
module UPS
NAME_KEYS = [:ups]
PRIMARY_NAME = 'UPS'
def self.fetch(number)
doc = Nokogiri::HTML(open("http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&InquiryNumber1=#{number}&track.x=0&track.y=0"))
table = doc.search('table[@class=dataTable]').first
latest_row = table.search('tr')[1]
if latest_row
cells = latest_row.search('td')
status = ShipmentStatus.new(
:number => number,
:location => cells[0].content.ircify_html,
:time => (cells[1].content + " " + cells[2].content).ircify_html,
:activity => cells[3].content.ircify_html,
:carrier => PRIMARY_NAME
)
else
nil
end
end # fetch UPS
end
module FedEx
NAME_KEYS = [:fedex]
PRIMARY_NAME = 'FedEx'
def self.fetch(number)
doc = open("http://www.fedex.com/Tracking?language=english&cntry_code=us&tracknumbers=#{number}")
data = ''
doc.each_line do |line|
if line =~ /^var detailInfoObject/
data = line
break # ignore the rest of this page
end
end
data = data.sub(/var detailInfoObject = /, '').sub(/;\n$/, '')
if data == ''
return nil
end
# keys = ["scanDate", "GMTOffset", "showReturnToShipper", "scanStatus", "scanLocation", "scanTime", "scanComments"]
data = JSON.parse(data)
latest_row = data['scans'].first
if latest_row
status = ShipmentStatus.new(
:number => number,
:location => latest_row['scanLocation'],
:time => latest_row['scanDate'] << ' ' << latest_row['scanTime'],
:activity => latest_row['scanStatus'],
:carrier => PRIMARY_NAME
)
else
nil
end
end # fetch fedex
end
module Purolator
NAME_KEYS = [:purolator]
PRIMARY_NAME = 'Purolator'
def self.fetch(number)
doc = Nokogiri::HTML(open("https://eshiponline.purolator.com/SHIPONLINE/Public/Track/TrackingDetails.aspx?pin=#{number}"))
latest_row = doc.search('//div[@id="detailTable"]/table/tbody/tr').first
if latest_row
latest_row = latest_row.search('./td')
status = ShipmentStatus.new(
:number => number,
:location => nil,
:time => (latest_row[0].inner_text + ' ' + latest_row[1].inner_text).gsub("\n", ' ').gsub(/\s+/, ' '),
:activity => latest_row[2].inner_text.gsub("\n", ' ').gsub(/\s+/, ' '),
:carrier => PRIMARY_NAME
)
else
nil
end
end
end
module Newegg
NAME_KEYS = [:newegg]
PRIMARY_NAME = 'newegg'
def self._get_json_obj(doc)
javascript_tags = doc.search('script[@type="text/javascript"]').
find_all {|x| x.to_s.include?('detailInfoObject') }
if javascript_tags.empty?
return nil
end
split_data = javascript_tags[0].text.strip.
split(";\r\n")[0].split("=", 2)
if split_data.size != 2
return nil
end
return JSON.parse(split_data[1].strip)
end
def self.fetch(number)
doc = Nokogiri::HTML(open("http://www.newegg.com/Info/TrackOrder.aspx?TrackingNumber=#{number}"))
latest_row = doc.search('table[@class="trackDetailUPSSum"]/tr')[1]
json_data = _get_json_obj(doc)
if json_data and json_data.include?('scans') and !json_data['scans'].empty?
latestScan = json_data['scans'][0]
ShipmentStatus.new(
:number => number,
:location => latestScan['scanLocation'],
:time => ("%s %s %s" % [
latestScan['scanDate'],
latestScan['scanTime'],
latestScan['GMTOffset']
]),
:activity => latestScan['scanStatus'],
:carrier => PRIMARY_NAME
)
elsif latest_row
datetime, location, activity = latest_row.search('td').map(&:text)
ShipmentStatus.new(
:number => number,
:location => location,
:time => datetime,
:activity => activity,
:carrier => PRIMARY_NAME
)
end
end
end
module PackageTrackr
NAME_KEYS = [:packagetrackr]
PRIMARY_NAME = 'packagetrackr'
def self.fetch(number)
doc = Nokogiri::HTML(open("http://www.packagetrackr.com/track/#{number}"))
latest_row = doc.search('//[class~="track-info-progress"]//tr')[0]
details = latest_row.children[0].inner_text.strip.split(/\n/)
if details
status = ShipmentStatus.new(
:number => number,
:location => details[0],
:time => details[1],
:activity => details[2],
:carrier => PRIMARY_NAME
)
else
nil
end
end
end
module USPS
NAME_KEYS = [:usps]
PRIMARY_NAME = 'USPS'
def self.fetch(number)
doc = Nokogiri::HTML(open("http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=#{number}"))
latest_row = doc.search('//table[@summary="This table formats the detailed results."]/tr')[1]
if latest_row
status = ShipmentStatus.new(
:number => number,
:location => nil,
:time => nil,
:activity => latest_row.inner_text.strip(),
:carrier => PRIMARY_NAME
)
else
nil
end
end
end
end
class ShipmentScreenScraperManager
def initialize()
@by_name = Hash.new
@loaded_modules = Array.new
end
def register(scraper_module)
for name in scraper_module::NAME_KEYS
if @by_name.has_key?(name.to_sym)
raise RuntimeException, "already registered."
end
@by_name[name.to_sym] = scraper_module
end
@loaded_modules << scraper_module
end
def [](name)
@by_name[name.to_sym]
end
def has_courier?(name)
@by_name.has_key?(name.to_sym)
end
def fetch(courier, number)
self[courier.to_sym].fetch(number)
end
end
def initialize()
super
@scrapers = ShipmentScreenScraperManager.new()
@scrapers.register(Scrapers::UPS)
@scrapers.register(Scrapers::FedEx)
@scrapers.register(Scrapers::Purolator)
@scrapers.register(Scrapers::Newegg)
@scrapers.register(Scrapers::PackageTrackr)
@scrapers.register(Scrapers::USPS)
end
attr_accessor :scrapers
def help(plugin, topic="")
"This plugin is a utility plugin which is only meant to be used by other plugins."
end
end
ShipmentTrackingUtilityPlugin.new
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment