#!/usr/bin/env ruby
require 'rubygems'
require 'bot'
require 'json'
require 'yaml'
require 'hpricot'
require 'open-uri'
require 'net/http'
# This version needs a patched version of jabberbot available at
# http://code.whomwah.com/ruby/bot.rb
# copy the bot.rb file into /path/to/gems/jabber-bot/lib/jabber/
CONFIG_SETTINGS = YAML::load(File.read('/home/duncan/configs/bbcbot.yaml'))
LAST_UPDATED = File.new(File.expand_path(__FILE__)).mtime
class BBC
DOMAIN = 'http://www.bbc.co.uk'
VERSION = '0.9.5'
class <<self
def http_fetch(url, log = nil)
URI.parse( File.join( DOMAIN, url ) ).open
rescue OpenURI::HTTPError => e
log.warn(e.inspect) if log
nil
end
def http_fetch_head(url, log = nil)
log.debug(url) if log
res = nil
Net::HTTP.start(URI.parse(DOMAIN).host) {|http|
res = http.head(url)
}
case res
when Net::HTTPSuccess
true
else
log.warn(res) if log
false
end
end
def parse_pid(opts)
'work in progress !'
end
def parse_a_z(opts)
doc = Hpricot(opts[:response].read)
res = [ "there are multiple results. try one of the more specific commands below:" ]
(doc/"#brands/li/a").each do |link|
res << "\n#{link.inner_html}"
pid = link.attributes['href'].split('/').last
res << programme_link(pid)
if opts[:show_in_detail]
end
res << "check #{pid}"
end
res.flatten.join("\n")
end
def parse_now(opts)
json = JSON.parse(opts[:response].read)
parse_programme( json["schedule"]["now"]["broadcast"]["programme"] )
rescue NoMethodError => e
opts[:log].error("now: str = #{opts[:response].read}") if opts.has_key? :log
raise e
end
def parse_next(opts)
json = JSON.parse(opts[:response].read)
parse_programme( json["schedule"]["next"]["broadcasts"][0]["programme"] )
rescue NoMethodError => e
opts[:log].error("next: str = #{opts[:response].read}") if opts.has_key? :log
raise e
end
def parse_programme(p)
[ "#{p["display_titles"]["title"]} :: #{p["short_synopsis"]}",
programme_link(p["pid"])
].join("\n")
end
def parse_schedule(opts)
json = JSON.parse(opts[:response].read)
date = Time.parse(json["schedule"]["day"]["date"]).strftime("%A %d %B %Y")
res = [ "#{json["schedule"]["service"]["title"]} Schedule, #{date}",
"#{ical_link(opts)}\n" ]
broadcasts = json["schedule"]["day"]["broadcasts"]
broadcasts.each do |b|
p = b["programme"]
title = display_title(p)
tstart = Time.parse(b["start"]).strftime("%H:%M")
tend = Time.parse(b["end"]).strftime("%H:%M")
res << "#{tstart}-#{tend} #{title}"
if opts[:show_in_detail]
res << [ p["short_synopsis"], programme_link(p["pid"]) ]
end
end
res.flatten.join("\n")
end
def parse_services(opts)
json = JSON.parse(opts[:response].read)
has_outlets = " OL = has outlets." unless opts[:show_in_detail]
res = [ "Available services.#{has_outlets}\n" ]
services = json["services"]
for s in services
op = service_outlet_text(s)
outlets = s["outlets"]
if !outlets.nil?
op = "#{op} OL"
end
res << op
next if !opts[:show_in_detail] or outlets.nil?
for ol in outlets
res << " #{service_outlet_text(ol)}"
end
end
res.compact.join("\n")
end
def parse_outlets(opts)
json = JSON.parse(opts[:response].read)
services = json["services"]
return "no outlets!" if services.empty?
for s in services
if opts[:message] == s["key"]
res = [ "Outlets for #{s["title"]}\n" ]
outlets = s["outlets"]
return "No outlets for this service" if outlets.nil?
for ol in outlets
res << "#{service_outlet_text(ol)}"
end
end
end
res.compact.join("\n")
end
def parse_upcoming(opts)
json = JSON.parse(opts[:response].read)
network_txt = " on #{opts[:network]}" unless opts[:network].nil?
res = [ "Here's some upcoming #{opts[:options].join('/')}#{network_txt} :-", ical_link(opts) ]
last_date = last_pid = nil
broadcasts = json["broadcasts"]
for b in broadcasts
p = b["programme"]
pid = p["pid"]
next if pid == last_pid
date = Time.parse(b["start"]).strftime("%A %d %B %Y")
tstart = Time.parse(b["start"]).strftime("%H:%M")
tend = Time.parse(b["end"]).strftime("%H:%M")
res << "\n#{date} :-" unless last_date == date
service = " [#{b["service"]["title"]}]" unless opts[:network]
synopsis = " :: #{p["short_synopsis"]}" if opts[:show_in_detail]
res << " #{tstart}-#{tend} #{display_title(p)}#{synopsis}#{service}"
res << programme_link(pid) if opts[:show_in_detail]
last_date = date
last_pid = pid
end
res.compact.join("\n")
end
def schoolboy_error(t = nil)
txt = ["Oops! Check your spelling, I can't quite understand what you're asking for."]
txt << "Maybe that network may require an <outlet>, use the command: services " \
"to double check." if t.nil?
txt.join(' ')
end
def programme_link(pid)
File.join( DOMAIN, "programmes", pid )
end
def ical_link(opts)
uri = opts[:response].base_uri
uri.scheme = "webcal"
str = uri.to_s
str.gsub(File.extname(str), '.ics')
end
def display_title(p)
"#{p["display_titles"]["title"]}"
end
def service_outlet_text(so)
"#{so["key"]} , [#{so["title"]}]"
end
end
end
if __FILE__ == $0
# Enable auto-flush on STDOUT
STDOUT.sync = true
# Create our new bot
bot = Jabber::Bot.new(
:name => 'BBC Programmes',
:jabber_id => CONFIG_SETTINGS[:settings][:jabberid],
:password => CONFIG_SETTINGS[:settings][:password],
:master => CONFIG_SETTINGS[:settings][:master],
:status => 'BBC Programmes available',
:is_public => true
)
# version information
bot.add_command(
:syntax => 'version',
:preamble => 'Current version and when last updated',
:description => "Displays the current version of the script\n" +
"running and when it was last updated",
:regex => /^version$/,
:alias => [
:syntax => 'v',
:regex => /^v$/
],
:is_public => true
) do |sender, message, log|
log.info("VERSION: #{message} | #{sender}")
"Version: #{BBC::VERSION}\nLast Updated: #{LAST_UPDATED}"
end
# Now
bot.add_command(
:syntax => 'now <network>[:<outlet>]',
:preamble => 'Displays which programme is currenly on',
:description => "Displays information about which programme\n" +
"is currently being broadcast on the <network>\n" +
"you provided, as well as providing a link to\n" +
"that programmes's webpage. Example usage:\n" +
"now radio1\n" +
"now 6music\n" +
"now bbcone:london",
:regex => /^now\s[a-z0-9]+\:?[a-z0-9]+?$/,
:is_public => true
) do |sender, message, log|
log.info("NOW: #{message} | #{sender}")
network, outlet = message.split(":")
url = File.join([network, "programmes/schedules", outlet, "upcoming.json"].compact)
log.debug("NOW: url: #{url}")
if response = BBC.http_fetch(url, log)
BBC.parse_now({ :response => response })
else
log.warn("NOW: schoolboy error!")
BBC.schoolboy_error(outlet)
end
end
# Next
bot.add_command(
:syntax => 'next <network>[:<outlet>]',
:preamble => 'Displays which programme is next on',
:description => "Displays information about which programme\n" +
"is next being broadcast on the <network> you\n" +
"provided, as well as displaying a link to\n" +
"that programmes's webpage. Example usage:\n" +
"next radio1\n" +
"next 6music\n" +
"next bbcone:london",
:regex => /^next\s[a-z0-9]+\:?[a-z0-9]+?$/,
:is_public => true
) do |sender, message, log|
log.info("NEXT: #{message} | #{sender}")
network, outlet = message.split(":")
url = File.join([network, "programmes/schedules", outlet, "upcoming.json"].compact)
log.debug("NEXT: url: #{url}")
if response = BBC.http_fetch(url, log)
BBC.parse_next({ :response => response })
else
log.warn("NEXT: schoolboy error!")
BBC.schoolboy_error(outlet)
end
end
# Schedule
bot.add_command(
:syntax => 'schedule <network>[:<outlet>][ in detail]',
:preamble => 'Displays a full day schedule',
:description => "Displays a full day schedule for the <network>\n" +
"you provided. By default you get a quick view,\n" +
"but by adding 'in detail' at the end you will get\n" +
"more detailed information about each programme.\n" +
"Example usage:\n" +
"schedule 6music\n" +
"schedule radio4:fm\n" +
"schedule bbcone:london in detail\n" +
"schedule radio1 in detail",
:regex => /^schedules?\s[a-z0-9]+(\:[a-z0-9]+)?(\sin\sdetail)?$/,
:is_public => true
) do |sender, message, log|
log.info("SCHEDULE: #{message} | #{sender}")
services, options = message.strip.split(' ', 2)
show_in_detail = (options =~ /in\sdetail/) ? true : false
network, outlet = services.strip.split(":").map { |s| s.strip }
url = File.join([network, "programmes/schedules", outlet].compact)
log.debug("SCHEDULE:\n" \
"show_in_detail: #{show_in_detail}\n" \
"url: #{url}")
if response = BBC.http_fetch("#{url}.json", log)
BBC.parse_schedule({
:show_in_detail => show_in_detail,
:response => response
})
else
BBC.schoolboy_error(outlet)
end
end
# Services
bot.add_command(
:syntax => 'services [in detail]',
:preamble => 'Displays a full list of networks',
:description => "Displays a full list of available networks\n" +
"<network> that can be used with many of the\n" +
"other commands I understand. Some networks\n" +
"require an outlet too, which can be found via\n" +
"the outlets commands. Example usage:\n" +
"services\n" +
"services in detail",
:regex => /^services(\s+)?(\sin\sdetail)?$/,
:is_public => true
) do |sender, message, log|
log.info("SERVICES: #{message} | #{sender}")
show_in_detail = (message =~ /in\sdetail$/) ? true : false
log.debug("SERVICES: show_in_detail = #{show_in_detail}")
if response = BBC.http_fetch("programmes/services.json")
BBC.parse_services({
:response => response,
:show_in_detail => show_in_detail
})
else
log.warn("SERVICES: Nothing found!")
"Hmm, strange, nothing found!"
end
end
# Outlets
bot.add_command(
:syntax => 'outlets <network>',
:preamble => 'Displays all the available outlets for a <network>',
:description => "Displays all the available outlets for a\n" +
"<network>. This is because an <outlet> is\n" +
"required for some network where there is more\n" +
"than one variation, for example:\n" +
"radio4 requires fm or lw to be decided, or\n" +
"bbcone has many regional variations\n" +
"You can tell if a <network> requires outlets, by\n" +
"using the services command and looking for OL\n" +
"next to the results. Example usage:\n" +
"outlets bbcone\n" +
"outlets bbctwo\n" +
"outlets radio1",
:regex => /^outlets\s[a-z0-9]+$/,
:is_public => true
) do |sender, message, log|
log.info("OUTLETS: #{message} | #{sender}")
if response = BBC.http_fetch("programmes/services.json")
BBC.parse_outlets({
:response => response,
:message => message
})
else
log.warn("OUTLETS: Nothing found!")
"Hmm, strange, nothing found!"
end
end
# upcoming
bot.add_command(
:syntax => 'upcoming <string>[:<string>][ on <network>][ in detail]',
:preamble => 'List upcoming programmes',
:description => "List upcoming programmes belonging to the\n" +
"<genre>, <format> or \"<query>\" passed in. You can also\n" +
"optionally filter by <network>. Add 'in detail' at\n" +
"the end of the command gives you more information back\n" +
"about each programmes. Example usage:\n" +
"upcoming films\n" +
"upcoming drama:crime\n" +
"upcoming sport on bbcone\n" +
"upcoming \"Top Gear\"\n" +
"upcoming \"Eastenders\" on bbcone\n" +
"upcoming drama:soaps on bbcone in detail" +
"upcoming \"doctor who\" on bbcone in detail" +
"upcoming \"later with jools holland\"",
:regex => /^upcoming\s([a-z]|"[\sa-zA-Z0-9]+")+(\:[a-z]+)?(\:[a-z]+)?(\son\s[a-z0-9]+)?(\sin\sdetail)?$/,
:is_public => true
) do |sender, message, log|
log.info("UPCOMING: #{message} | #{sender}")
show_in_detail = (message =~ /\sin\sdetail$/) ? true : false
message = message.gsub(' in detail', '').strip if show_in_detail
options, network = message.split(/\bon\b/).map {|a| a.strip}
options = options.split(':').map { |f| f.strip }
query = (options.first =~ /"[\sa-zA-Z0-9]+"/)
unless query
genre_url = File.join([network, "programmes/genres", options, "schedules/upcoming"].compact)
format_url = File.join([network, "programmes/formats", options, "schedules/upcoming"].compact)
end
brand_url = File.join(["programmes", URI.escape(options.join.gsub("\"",''))].compact)
log.debug("UPCOMING:\n" \
"show_in_detail: #{show_in_detail}\n" \
"found_search_string: #{query ? true : false}\n" \
"genre: #{genre_url}\n" \
"format: #{format_url}\n" \
"brand: #{brand_url}")
# are we dealing with a genre
if !query && response = BBC.http_fetch_head("/#{genre_url}.json", log)
log.debug("UPCOMING: found genre, #{options}")
if response = BBC.http_fetch("#{genre_url}.json", log)
BBC.parse_upcoming({
:response => response,
:options => options,
:network => network,
:show_in_detail => show_in_detail
})
else
BBC.schoolboy_error(outlet)
end
# are we dealing with a formt
elsif !query && response = BBC.http_fetch_head("/#{format_url}.json", log)
log.debug("UPCOMING: found format, #{options}")
if response = BBC.http_fetch("#{format_url}.json", log)
BBC.parse_upcoming({
:response => response,
:options => options,
:network => network,
:show_in_detail => show_in_detail
})
else
BBC.schoolboy_error(outlet)
end
# is this a brand
elsif response = BBC.http_fetch(brand_url)
url = response.base_uri.to_s
log.debug("UPCOMING: #{url}")
if url =~ /a\-z/
BBC.parse_a_z({
:response => response,
:show_in_detail => show_in_detail
})
else
pid = url.split('/').last
brand_url = File.join(["programmes", pid, "episodes/upcoming"].compact)
if response = BBC.http_fetch("#{brand_url}.json", log)
BBC.parse_upcoming({
:response => response,
:options => options,
:network => network,
:show_in_detail => show_in_detail
})
else
"Sorry, nothing found!"
end
end
else
log.warn("UPCOMING: nothing understood, nothing returned.")
"Sorry, I can't seem to find anything right now"
end
end
# PID
bot.add_command(
:syntax => 'check <pid>',
:preamble => 'Displays information about a programme',
:description => "Displays information about a single programme",
:regex => /^check\s[a-z0-9]+$/,
:is_public => true
) do |sender, message, log|
log.info("PID: #{message} | #{sender}")
pid = message
if response = BBC.http_fetch("programmes/#{pid}")
BBC.parse_pid({
:response => response,
:pid => pid
})
else
log.warn("PID: Nothing found!")
"Hmm, strange, nothing found!"
end
end
# start the bot
bot.connect
end