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