Last active
December 21, 2015 19:09
-
-
Save gyng/6352421 to your computer and use it in GitHub Desktop.
shipping EXPERT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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