Toodledo iCal v2

Friday, 23 October 2009 01:04 PM
by Coose

I’m going to make this short, because I just typed up a full post and the piece of shit Windows Live Writer crashed…again…and I lost everything and don’t feel like typing it all again.

In a previous post, I described an iCal feed that I created to enhance the functionality of Toodledo’s iCal feed.  To restate the problem: The Toodledo iCal feed creates an all-day event for each task that has a due date.  The problem is that if a task overdue and not completed, it should show up on the current day, not the original due date.  Windows Mobile applications like Agenda One and Pocket Informant use this functionality, and I really got used to it.  Toodledo tasks are always displayed on the calendar feed on their original due date.

So, what I need to do is if the task is incomplete and overdue, modify the due date to today.  At first glance I thought of using the Toodledo API to create the calendar feed manually.  The problem is I have three young kids at home, and a million things on my plate.  So I need a quick and dirty solution.

What I originally opted to do is create a handler on my own web site to serve up the ics feed.  The handler would call the Toodledo ics feed, and modify any overdue start and end date in the resulting stream and set it to the current date.  Quick, easy, and effective.  Perfect.  Examining the ics feed manually, and testing in Sunbird and Outlook showed everything A-OK.

But when adding the feed to Google, it originally shows up correctly, but overdue tasks do not show up on the current day, like I coded.  Google does not allow a refresh of the subscribed calendars, so there’s no way to test immediately.  But after watching a few days, it seems that the calendar appears to cached on Google servers.  I think what I need to do is modify the start and end date of expired incomplete tasks, as well as the last modified date.  This should cue Google to update the calendar item.

So, the first thing to do is see what the original feed looks like.  Using one of the most useful tools in my toolbox, LINQPad, I we can see the resulting stream.  The address of the ics from Toodledo can be found by logging in to Toodledo, and navigating to http://www.toodledo.com/connect_ical.php.

The LINQPad statement to show the result is:
Encoding.ASCII.GetString(new WebClient().DownloadData("http://www.toodledo.com/id/tdXXXXXXXXXXXXX/ical_live.ics")).Dump();
Again, your URL will differ.  Make sure to change the “webcal://” protocol to “http://” as WebClient doesn’t understand what “webcal://” is.

The resulting data is:
BEGIN:VCALENDAR
PRODID:-//Toodledo iCal//www.toodledo.com//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Toodledo iCal
LAST-MODIFIED:20091023T220858Z
BEGIN:VEVENT
URL:http://www.toodledo.com
CREATED:20090822T000000
DTSTAMP:20090822T000000
LAST-MODIFIED:20090927T205903
UID:some-guid#toodledo.com
SUMMARY:Task Name
DESCRIPTION:
PRIORITY:5
STATUS:CONFIRMED
TRANSP:OPAQUE
DTSTART;VALUE=DATE:20090927
DTEND;VALUE=DATE:20090927

END:VEVENT
END:VCALENDAR

Toodledo only returns incomplete tasks in its feed, so we don’t have to worry about filtering on the status.  The lines in red are the lines that need to be modified for our customized feed.

So, create a handler for your feed name derived from IHttpHandler.  Override the ProcessRequest method to parse each VEVENT and if needed, adjust the dates.

    1 public void ProcessRequest(HttpContext context)

    2 {

    3     WebClient client = new WebClient();

    4     string toodledo = Encoding.ASCII.GetString(client.DownloadData("http://www.toodledo.com/id/tdXXXXXXXXXXXXX/ical_live.ics"));

    5 

    6     List<Dictionary<string, string>> items = new List<Dictionary<string, string>>();

    7 

    8     foreach (Match match in Regex.Matches(toodledo, @"BEGIN:VEVENT.*?END:VEVENT", RegexOptions.Singleline))

    9     {

   10         Dictionary<string, string> values = new Dictionary<string, string>();

   11 

   12         // now we have a bunch of name/value pairs

   13         foreach (string line in match.Value.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))

   14         {

   15             int x = line.IndexOf(':');

   16             if (x < 0)

   17             {

   18                 values[line] = string.Empty;

   19             }

   20             else

   21             {

   22                 values[line.Substring(0, x)] = (x >= line.Length - 1) ? string.Empty : line.Substring(x + 1);

   23             }

   24         }

   25 

   26         items.Add(values);

   27     }

   28 

   29     // adjust dates.

   30     // when adjusting the date, make sure to adjust modified date, etc.  Google seems to need this.

   31     foreach (Dictionary<string, string> item in items)

   32     {

   33         string[] keys = new string[] { "DTSTART;VALUE=DATE", "DTEND;VALUE=DATE" };

   34         foreach (string key in keys)

   35         {

   36             if (item.ContainsKey(key))

   37             {

   38                 DateTime currentDate = DateTime.ParseExact(item[key], "yyyyMMdd", CultureInfo.InvariantCulture);

   39                 if (currentDate < DateTime.Now)

   40                 {

   41                     item[key] = DateTime.Now.ToString("yyyyMMdd");

   42                     item["LAST-MODIFIED"] = DateTime.Now.ToString("yyyyMMddThhmmss");

   43                     item["DTSTAMP"] = DateTime.Now.ToString("yyyyMMddThhmmss");

   44                 }

   45             }

   46         }

   47     }

   48 

   49     StringBuilder sb = new StringBuilder();

   50 

   51     sb.AppendLine("BEGIN:VCALENDAR");

   52     sb.AppendLine("PRODID:-//Toodledo iCal//www.toodledo.com//EN");

   53     sb.AppendLine("VERSION:2.0");

   54     sb.AppendLine("CALSCALE:GREGORIAN");

   55     sb.AppendLine("METHOD:PUBLISH");

   56     sb.AppendLine("X-WR-CALNAME:Toodledo iCal");

   57     sb.Append("LAST-MODIFIED:");

   58     sb.Append(DateTime.UtcNow.ToString("yyyyMMddThhmmssZ"));

   59     sb.AppendLine();

   60 

   61     foreach (Dictionary<string, string> item in items)

   62     {

   63         foreach (KeyValuePair<string, string> pair in item)

   64         {

   65             sb.AppendFormat("{0}:{1}", pair.Key, pair.Value);

   66             sb.AppendLine();

   67         }

   68     }

   69 

   70     sb.AppendLine("END:VCALENDAR");

   71 

   72     context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

   73     context.Response.Headers["Keep-Alive"] = "timeout=2, max=100";

   74     context.Response.Headers["Connection"] = "Keep-Alive";

   75     context.Response.ContentType = client.ResponseHeaders[HttpResponseHeader.ContentType];

   76     context.Response.Output.Write(sb.ToString());

   77 }

A quick overview of what is happening:

  • Line 4 downloads the full original feed from Toodledo.
  • Lines 6 – 27 find all text between “BEGIN:VEVENT” and “END:VEVENT”, and for each line adds a dictionary entry with the text to the left of the : as the key, and to the right as the value.
  • Lines 31 – 47 find any DTSTART;VALUE=DATE or DTEND;VALUE=DATE entries, parses the value into a date, and changes the date to today if the date is in the past, and also sets LAST-MODIFIED and DTSTAMP to today.
  • Lines 51 – 59 write the standard iCal information.
  • Line 58 changes the modified date of the entire iCal feed to today.
  • Lines 61 – 68 write each name/value pair of the dictionary back to the iCal.
  • Line 70 closes the iCal.
  • Lines 72 – 76 writes the iCal to the response stream.

Publish this handler on your web site, and add the URL of YOUR HANDLER to Google calendar, and your Toodledo tasks are “fixed”.

Comment on this
Mike's Blog
|

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading