#!/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 < 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 , 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 [:]', :preamble => 'Displays which programme is currenly on', :description => "Displays information about which programme\n" + "is currently being broadcast on the \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 [:]', :preamble => 'Displays which programme is next on', :description => "Displays information about which programme\n" + "is next being broadcast on the 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 [:][ in detail]', :preamble => 'Displays a full day schedule', :description => "Displays a full day schedule for the \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" + " 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 ', :preamble => 'Displays all the available outlets for a ', :description => "Displays all the available outlets for a\n" + ". This is because an 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 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 [:][ on ][ in detail]', :preamble => 'List upcoming programmes', :description => "List upcoming programmes belonging to the\n" + ", or \"\" passed in. You can also\n" + "optionally filter by . 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 ', :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