iCal - trigger script and act programmatically on event or todo

Dokumentation is extensive. Code is in fact pretty simple

See also here: http://macscripter.net/viewtopic.php?id=31833

Edit 2010.01.23: Version 1.1
Edit 2010.01.24: Version 1.2
Edit 2010.01.25: Version 1.3
Edit 2010.01.25: Version 1.3.1

--  ============
--    iCal_Growl_Alarm
--  ============
--  Version 1.3.1
--  adopted and marginally adapted by maelcum
--  all credits go to the original developers (see Sources), two of whom have created nearly identical solutions (not really, you'll see who copied from whom easily).
--  Sources:
--    -  http://software.imagn.net/
--    -  http://sg80bab.blogspot.com/2008/03/applescript-growl-and-ical.html
--    -  http://macscripter.net/viewtopic.php?id=31833
--  Version History:
--    1.1
--    -  Switched from preferences.plist-file to AppleScript properties
--    -  Reorganized Growl-Initialization into single handler growlInstalled()
--    1.2
--    -  Now iterating through all possible alarms per event to find the one which called us
--    1.3
--    - Also checking ToDos - without any time frame limitations
--    1.3.1
--    - Ignoring completed Todos

--  ===========
--   About this Script
--  ===========
--  This script should be called from iCal via "Run Script". It then identifies which event has triggered it and acts as programmed.
--  Think of it as a framework for the processing of iCal events.
--  For my needs, it generates a message via Growl about the event that has called it.
--  Kind of a laymans Growl-support for iCal.
--  With Growls' support of Prowl, this means you could flexibly forward certain iCal alarms of events or todos to your iPhone.
--  But since AppleScript is so wonderfully flexible, it's dead easy to extend it to send those alarms to twitter, facebook, whatever.
--  iCal allows one or more alarms for each entry (events or to do items).
--  Each alarm can show a message (with or without sound), send an email, open a file or run a script.
--  To do something automated, running an AppleScript is perfect.
--  Unfortunately, iCal does not pass any information about the event to a script.
--  In order to do something meaningful, it is necessary to identify the corresponding event, extract and mangle the information.
--  To do so, each calender is searched.
--  That could be thousands of events; searching all of them could make this script very slow. So only a certain time frame is checked and certain calendars can be excluded.
--  See "calendars_to_ignore" below for a customization of exclusions.
--  Drawback about using a time frame:
--  This will lead to failure if the alarm is triggered long before the actual event is due.
--  Example: 
--    -  An event is set for 7 o'clock.
--    -  The alarm is set to go off 10 days in advance.
--    -  The time frame is set to 5 days.
--  When the alarm goes off, iCal launches this script. This script will look 5 days in the future to find all events at 7 o'clock.
--  If there *is none*, the script will do nothing.
--  If there *are* one or more events, also scheduled for 7 o'clock, this script check the alarm lead time (of 10 days). Since that does not sum up properly, the events will all be discarded.
--  No Growl messages in either case.
--  For a reasonable balance between speed and my normal usage pattern (there are few events that I need to have triggered more than two weeks in advance) the current time frame is set to 15 days.
--  Feel free to change that to your liking; see "time_frame_end" below.
--  If you happen to use alarms as a reminder *after* the event has happened then you need to change the value of "time_frame_start" as well.
--  Remember to check the scripts speed after any changes. I'd recommend to trim the time frame start and end values if the runtime is longer than about 10 seconds.
--  All repeating alarms that have been *entered* (sic!) before the time frame will *not* be recognized!
--  Let me emphasize this: 
--  ALL REPEATING ALARMS (like Birthdays or your weekly tennis match) WILL MOST LIKELY NOT BE DETECTED.
--  That's because iCal (to me mysterious) reason simply does not detect these events as being in the time frame.
--  It looks as if iCal is checking the time stamp of the entry vs. the start date of the entry.
--  It's a bummer, I know...
--  For ToDos, I'm assuming that there aren't that many (say, more than 50) so searching will be lightning fast, no matter how big the time frame is or how many calendars there are.
--  For that reason, there is no time frame, nor are any calendars ignored.

--  ==============
--   Additional information
--  ==============
--  The following is more an explanation about the why and how for myself, rather than written as a proper documentation of any sort for anybody else.
--  Any reader will probably find it lengthy at times, especially if proficient in AppleScript, and too terse at others, where I am experienced enough not to document the obvious - to me.
--  If in doubt, feel free to ask.
--  That said, this script comes with no promise whatsoever. It might make your neighbours' cat go crazy or the esteemed reader go bald. I'm just glad it works for me.
--    About Growl
--  -------------------
--  Growl is a great way to give notice. While somebody is at the machine, messages just pop up a defined length of time.
--  If not, sticky messages make it easy to see what has happened during that time. Perfect for something like this script.
--  On top, it is able to "prowl" messages to the so-called iPhone App, thus passing on any message if I'm not at the machine.
--  Each new script that wants to access Growl, needs to be registered with Growl.
--  Registration expects some arguments and can be repeatedly done.
--  I value that as bad style, since this script is expected to run multiple times a day (or hour, if you happen to have a lot of iCal entries with alarms).
--  Therefor the state of the registration is written to an AppleScript property and checked before being done again.

----  ===============
----  Declaration of Properties
----  ===============

--  Installation-specific string
property growl_is_registered : false
set calendars_to_ignore to {"foo","bar"} --  This is a list of calendars that should not be searched for an event.

--  Language-specific strings
set lang_minutes to " min"

--  Static strings
global current_os_x_version
set current_os_x_version to (get version of application "Finder") as string --  Get version of OS X this script is currently running on
property growl_notification_application_name : "iCalGrowl-Script" --  This is the name shown in the Growl Preference Pane
property growl_notification_application_icon : "AppleScript Editor" --  The Name of the Editor for AppleScript is "Script Editor" until and including 10.5 and "AppleScript Editor" from 10.6 on.
property growl_notification_types_list : {"Entry"} -- A list of all the notification types that this script will ever send. Each notification be activated/deactivated as needed and can have different properties (can be set in the Growl Preference Pane)
property growl_priority_very_low : -2 --  Use this for debugging messages
property growl_priority_moderate : -1 --  Use this for everything that is reported "as planned"
property growl_priority_high : 1 --  Use this for all messages that are out of the ordinary
property growl_priority_emergency : 2 --  Use this to get the users' attention

--  Definition of the time frame in which to search for an event
set current_date to current date
set time_frame_start to current_date - 2 * hours --  Look two hours back in time, like when the machine just woke up from sleep...
set time_frame_end to current_date + 15 * days --  Look ahead 15 days into the future...

----  ===============
----  Declaration of Functions
----  ===============

--  Check if Growl is installed and register this script, if not already done so
on growlInstalled()
	--  Check if Growl is installed
	tell application "System Events"
		if exists application process "GrowlHelperApp" then
			if growl_is_registered is false then
				if current_os_x_version starts with "10.5" then set growl_notification_application_icon to "Script Editor"
				tell application "GrowlHelperApp" to ¬
					register as application growl_notification_application_name ¬
						all notifications growl_notification_types_list ¬
						default notifications growl_notification_types_list ¬
						icon of application growl_notification_application_icon
				set growl_is_registered to true
			end if
			return true
			return false --  No Growl installed
		end if
	end tell
end growlInstalled

----  ===============
----    ==== on run ====
----  ===============

--  The main routine:
--    -  gets one calender after the other
--      -  gets all events for each calendar
--        -  check for each event if it has an alarm set (if not, it won't be the one having triggered this script)
--          -  if so, checks if the current time plus the alarm lead time is equal to the event start time (± 1 minute)
--               and if so, the event that has triggered this script has been found, so
--            -  get title, location and description
--            -  and hand it over to Growl for display
tell application "iCal"
	--  Search through all calendars, one by one
	set all_calendars to every calendar
	repeat with current_calendar in all_calendars
		--  Ignore calendars to speed up this script
		if (name of current_calendar) is not in calendars_to_ignore then
			--  Wade through all events in each calendar that occur in the defined time frame
			--  !! All repeating alarms that have been *entered* (into iCal) (sic!) before this time frame will *not* be recognized !!
			set all_events_of_current_calendar to (every event of current_calendar ¬
				whose ((start date ≥ time_frame_start and start date ≤ time_frame_end) ¬
				or (end date ≥ current_date and start date ≤ time_frame_start)))
			repeat with current_event in all_events_of_current_calendar
				--  Check if the current event has an "open file" (=run script) alarm assigned?
				if exists open file alarm of current_event then
					--  Since each event can have more than one alarm, wade through each one of them as well
					set all_ofalarms_of_current_event to every open file alarm of current_event
					repeat with current_ofalarm in all_ofalarms_of_current_event
						set current_event_title to ""
						set current_event_description to ""
						set current_event_location to ""
						set ofalarm_lead_time to trigger interval of current_ofalarm --  How long before/after the event in minutes is the alarm set?
						--  Check if the alarm happens to start in "now + ofalarm_lead_time (± 1min)". If so, we have a winner!
						--  Remember: The ofalarm_lead_time is negative, if the alarm is set before the event! (And unfortunately named, if the alarm is set after the event...)
						if ((start date of current_event is greater than (current_date - (ofalarm_lead_time * minutes) - (1 * minutes))) ¬
							and (start date of current_event is less than (current_date - (ofalarm_lead_time * minutes) + (1 * minutes)))) then
							--  Found it!
							tell current_event
								if exists summary then set current_event_title to (summary & " in " & (ofalarm_lead_time * -1) & lang_minutes) --  Get the absolute value (ABS) for ofalarm_lead_time
								if exists description then set current_event_description to description
								if exists location then set current_event_location to location
							end tell
							--  Have Growl display a message
							if (my growlInstalled()) then ¬
								tell application "GrowlHelperApp" to notify application name growl_notification_application_name with name "Entry" title current_event_title description current_event_location & " - " & current_event_description priority growl_priority_high with sticky
						end if
					end repeat
				end if
			end repeat
		end if --  If calendars should also be ignored for Todos, move this down after the Check of Todos
		--  Check the uncompleted Todos Alarms.
		set all_todos_of_current_calendar to (every todo of current_calendar)
		repeat with current_todo in all_todos_of_current_calendar
			--  Ignore completed Todos to speed up script
			if not (exists completion date of current_todo) then
				if exists open file alarm of current_todo then
					set all_ofalarms_of_current_todo to every open file alarm of current_todo
					repeat with current_ofalarm in all_ofalarms_of_current_todo
						set current_todo_title to ""
						set current_todo_description to ""
						set current_todo_location to ""
						set ofalarm_lead_time to trigger interval of current_ofalarm
						if ((start date of current_todo is greater than (current_date - (ofalarm_lead_time * minutes) - (1 * minutes))) ¬
							and (start date of current_todo is less than (current_date - (ofalarm_lead_time * minutes) + (1 * minutes)))) then
							tell current_todo
								if exists summary then set current_todo_title to (summary & " in " & (ofalarm_lead_time * -1) & lang_minutes)
								if exists description then set current_todo_description to description
								if exists location then set current_todo_location to location
							end tell
							if (my growlInstalled()) then ¬
								tell application "GrowlHelperApp" to notify application name growl_notification_application_name with name "Entry" title current_todo_title description current_todo_location & " - " & current_todo_description priority growl_priority_high with sticky
						end if
					end repeat
				end if
			end if
		end repeat
	end repeat
end tell