This isn''t even close to ready for release, but in case anybody is interested, has feedback, finds it useful as is, or even wants to collaborate... I''ve been working on a "personal information server", something of a protocol multiplexer. Right now it serves calendar feeds for: - a local calendar folder (iCal 1.x''s Library/Calendars) - rss for local todos - icalendar for bdays in a vcard file I run it as a LoginHook on OS X, haven''t tried on Linux. Its a longterm project, with more possibilities than I have time, but I conceive of it taking information from various sources, and presenting them back using rss, ical, jabber, LDAP, webdav/caldave, etc.. The idea is people have favorite tools and browsers. Some people like to see everything as RSS, others like to see everything in a calendar, etc., and much information can be reperesented in many ways, but often the provider of the info doesn''t think of that. The reason I wrote net-mdns is because I wanted this server to be able to advertise services using Rendezvous/Bonjour/DNS-SD. As an example, I can''t import .mp3 files of the Great Eastern as an iTunes podcast, because they aren''t a podcast, just a php site. But, I could build a web scraper that represented all the .mp3s found on a site as an RSS/podcast, and then import them with iTunes, and have iTunes notify me when new ones get added. I think the possibiliites are endless, here''s the ideas I''ve collected from my TODO: WARNING -- If you run vpimd, note that it basically has no security, authorization, etc. It might be a huge privacy hole! Note: - WebDAV client-side: - http://raa.ruby-lang.org/project/libneon-ruby/ - http://www.webdav.org/neon/ - WebDAV server-side: - implement a WEBRick servlet to handle server-side? todo: growl-to-* todo: *-to-growl Q: should growls go to all servers, or just specific ones? - http://raa.ruby-lang.org/project/ruby-growl/ todo: soap/weather-to-ics Query weather reports using SOAP, represent as iCalendars. todo: ics-alarm A really, really fast way of adding an alarm to iCalendar, i.e., 3 min, check the kettle. Input: - a time offset, and an action Output: - .ics - ical todo: local-to-rss Represent any collection of local files as a podcast, so it can be imported into iTunes as a podcast. todo: http-to-rss Also, any web location, crawl it for audio, and represent as a podcast. todo: ics-to-rss Input: - ~/Library/Calendars/ - .ics, ../*.ics Output: - CGI - stdout - http Options: - rss versions - rss versions in the URL - implement autodiscovery with http? what does this mean... todo: rss-to-jabber todo: ics-to-ics split calendars by date todo: ics-to-ics Input: x iCal 1 - iCal 2 - .ics Output: - http (subscribable) - CAP - WebDAV Options: - particular calendars x Rendezvous - StartupItem - all users, if they have a config file, like web pages - combine different calendars, and publish to .mac todo: rss-to-rss See: feedblender - http://feedblender.rubyforge.org todo: vcf-to-address-book Input: - .vcf - AB Output: - LDAP? AB can subscribe to, at least, ldap sources. Can I use this? Can the latest AB have plugins? done: mutt-aliases-to-vcf Input: - mutt aliases Output - .vcf todo: rss-to-ics convert rss feed, text or URI, to ics file of todos Input: - .xml - http - http with autodiscovery? Output: - http: a subscribable calendar - .ics Options: - RSS -> VTODO - RSS -> VEVENT - RSS -> VJOURNAL Notes: - will need a decent ics encoding api todo: vcf-bday-to-ics.rb Input: - .vcf - AB Output: - .ics - http - RSS??! maybe only see bdays within the next month, as a reminder? - cvt vcf file to ics file of birthdays - pull from Address Book, or from .vcf file Options: - bday -> VTODO - bday -> VEVENT - bday + VALARM todo: - an application server (with plugins?) that can do ALL of the above -------------- next part -------------- #!/opt/local/bin/ruby -w require ''etc'' require ''webrick'' require ''rss/maker'' require ''vpim/icalendar'' require ''vpim/vcard'' # Can we redirect IO to syslog? STDOUT.reopen("/Users/sam/vpimd.err", "w") STDERR.reopen(STDOUT) puts "here 0" # OS X Login Windows will run Login Hooks as root, with first argument being # the user''s login name. Drop root-privilege. # # Notes: # - LoginHook must be set system-wide (with sudo), or it won''t run. From OS 10.? on. # - LoginHook must return to the Login Window for login to resume. if ARGV[0] && Process.uid == 0 # Set environment up based on specified user pw = Etc.getpwnam(ARGV[0]) ENV[''HOME''] = pw.dir ENV[''USER''] = pw.name # ENV[''SHELL''] = pw.shell? # ENV[''PATH''] = ? Dir.chdir pw.dir Process::Sys.setgid pw.gid Process::Sys.setuid pw.uid # or perhaps: # Process.initgroups # Process.gid # Process.uid if pid = Kernel.fork puts "Child #{pid}" exit 0 end end $LOG = Dir.getwd + "/vpimd.log" puts "Open log: #{$LOG}" # Reopen io to somewhere useful. unless STDOUT.isatty STDOUT.reopen($LOG, "w") STDERR.reopen(STDOUT) end system "/usr/bin/id" system "/usr/bin/env" # Notes on debugging with dnssd API: check system.log, it should give info. #require ''pp'' #module Kernel # def to_pp # s = PP.pp(self, '''') # s.chomp! # s # end #end #-------------------------------------------------------------------------------- # Load DNSSD support, if possible. # TODO - should be in net/dns/dnssd # FIXME - doesn''t work with dnssd @avoid_native = true begin if @avoid_native raise LoadError else require ''dnssd'' end rescue LoadError begin require ''net/dns/mdns-sd'' DNSSD = Net::DNS::MDNSSD rescue LoadError DNSSD = nil end end #-------------------------------------------------------------------------------- # Set up server environment. # Find the user''s name and host. require ''etc'' $user = Etc.getpwuid(Process.uid).gecos $host = Socket.gethostname $services = [] $stuff = [] def register(name, path, protocol = ''http'') # $services << DNSSD.register(name, ''_http._tcp'', ''local'', $port, ''path'' => path ) puts "register #{name.inspect} on path #{path.inspect} with #{protocol}" $stuff << [name, path, protocol] end # Create a HTTP server, possibly on a dynamic port. # - Dynamic ports don''t actually work well. While it is true we can advertise them, # non-DNSSD aware clients will store the hostname and port when they subscribe to # a calendar, but when the server restarts the port will be different. Not good. $port = 8191 server = WEBrick::HTTPServer.new( :Port => $port ) if $port == 0 # Server may have created multiple listeners, all on a different dynamically # assigned port. families = Socket.getaddrinfo(nil, 1, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_PASSIVE) listeners = [] families.each do |af, one, dns, addr| listeners << TCPServer.new(addr, $port) $port = listeners.first.addr[1] unless $port != 0 end listeners.each do |s| puts "listen on #{s.addr.inspect}" end # So we replace them with our TCPServer sockets which are all on the same # (dynamically assigned) port. server.listeners.each do |s| s.close end server.listeners.replace listeners server.config[:Port] = $port end server.config[:MimeTypes][''ics''] = ''text/calendar'' server.config[:MimeTypes][''vcf''] = ''text/directory'' #-------------------------------------------------------------------------------- # Mount services ##### Vcard Birthdays as iCalendar $vcf_bday_file = ''vpim-bday.vcf'' $vcf_bday_path = ''/vcf/bday.ics'' class VcfBdayIcsServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, resp) cal = Vpim::Icalendar.create open($vcf_bday_file) do |vcf| Vpim::Vcard.decode(vcf).each do |card| begin bday = card.birthday if bday cal.push Vpim::Icalendar::Vevent.create_yearly( card.birthday, "Birthday for #{card[''fn''].strip}" ) $stderr.puts "#{card[''fn'']} -> bday #{cal.events.last.dtstart}" end rescue $stderr.puts $! $stderr.puts $!.backtrace.join("\n") end end end resp.body = cal.encode resp[''content-type''] = ''text/calendar'' # Is this necessary, or is it default? raise WEBrick::HTTPStatus::OK end end server.mount( $vcf_bday_path, VcfBdayIcsServlet ) register( "Calendar for all the Birthdays in my vCards", $vcf_bday_path, ''webcal'' ) ##### iCalendar as calendars # Export local calendars two different ways $ical_folder = File.expand_path( "~/Library/Calendars" ) # # Here we write a servlet to display all the allowed calendars with # webcal links, so they open in iCal. # $ical_include = /^[A-Z]/ $ical_title = "My Calendars" class IcalIcsServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, resp) body = '''' # body << @options.inspect folder, include, exclude = *@options path = req.path_info # body << "\n" # body << "path=#{path.inspect}\n" all = Dir.entries(folder).select do |f| f =~ /\.ics$/ end if include all.reject! do |f| !(f =~ include) end end if exclude all.reject! do |f| f =~ exclude end end # body << "#{all.inspect}\n" if(path == '''') body << "<ul>\n" all.each do |f| n = f.sub(''.ics'', '''') body << "<li><a href=\"webcal://#{$host}:#{$port}/calfile/#{f}\">#{n}</a>\n" end body << "</ul>\n" end resp.body = body resp[''content-type''] = ''text/html'' raise WEBrick::HTTPStatus::OK end end server.mount( ''/calweb'', IcalIcsServlet, $ical_folder, $ical_include) register( "My Calendars as webcal:// Feeds", ''/calweb'' ) # # We use the WEBrick file servlet to actually serve calendar files. # FIXME - this means that if you guess someone''s calendar name, you can # download it, despite the rudimentary security. # server.mount( ''/calfile'', WEBrick::HTTPServlet::FileHandler, $ical_folder, :FancyIndexing=>true ) # For debugging... register( ''My Calendar Folder'', ''/calfile'' ) ##### iCalendar/todo as RSS $ics_todo_title = ''My Todo Items as an RSS Feed'' $ics_todo_path = "/ics/todo.rss" class IcalTodoRssServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, resp) rss = RSS::Maker.make("0.9") do |maker| title = $ics_todo_title link = ''http:///'' maker.channel.title = title maker.channel.link = link maker.channel.description = title maker.channel.language = ''en-us'' # These are required, or RSS::Maker silently returns nil! maker.image.url = "maker.image.url" maker.image.title = "maker.image.title" Dir[ $ical_folder + "/*.ics" ].each do |file| # todo: use the open with a block variant Vpim::Icalendar.decode(File.open(file)).each do |cal| cal.todos.each do |todo| if !todo.status || todo.status.upcase != "COMPLETED" item = maker.items.new_item item.title = todo.summary item.link = todo.properties[''url''] || link item.description = todo.description || todo.summary end end end end end resp.body = rss.to_s resp[''content-type''] = ''text/xml'' raise WEBrick::HTTPStatus::OK end end server.mount( $ics_todo_path, IcalTodoRssServlet ) register( $ics_todo_title, $ics_todo_path ) #-------------------------------------------------------------------------------- ## Top-level page. $vpim_title = "vPim for #{$user}" class VpimServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, resp) body = <<"EOF" <h1>#{$vpim_title}</h1> You can access: <ul> EOF $stuff.each do |name,path,protocol| body << "<li><a href=\"#{protocol}://#{$host}:#{$port}#{path}\">#{name}</a>\n" end body << "</ul>\n" resp.body = body resp[''content-type''] = ''text/html'' raise WEBrick::HTTPStatus::OK end end server.mount( ''/'', VpimServlet ) #-------------------------------------------------------------------------------- # Run server $services << DNSSD.register($vpim_title, ''_http._tcp'', ''local'', $port, ''path'' => ''/'' ) [''INT'', ''TERM''].each do |signal| trap(signal) do server.shutdown $services.each do |s| s.stop end end end server.start # TODO - cleanup err.log on success