MacRuby – Create iCal event with alarm from Ruby

I’ve had to create some very similar events in my calendar, but they did not match a valid recurrence pattern. As I find it quite painful to create multiple iCal events I thought I maybe should try out MacRuby to automate the process as far as I could. As the resources for this topic are rare on the net I thought I could share the basics. I’ll explain the details below the coding.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/local/bin/macruby
framework 'calendarstore'
 
defStore = CalCalendarStore.defaultCalendarStore
 
puts "Select the calendar:"
defStore.calendars.each_with_index do |cal,i|
  puts "\t#{i}. #{cal.type}\t#{cal.title}"
end
 
puts "Type in the number for the calendar you wish to use:"
cal_index = gets.to_i
 
puts "Creating an event..."
 
# setting up the basics
e = CalEvent.new
e.calendar = defStore.calendars[cal_index]
e.title = 'Just a macruby test'
e.startDate = Time.now + 3600
e.endDate = Time.now + 7200
 
# creating the alarm
a = CalAlarm.alarm
a.action = CalAlarmActionSound
a.sound = "Basso"
a.relativeTrigger = -60 * 60 # negative seconds
e.addAlarm a
 
# save the event
errorPointer = Pointer.new :object
success = defStore.saveEvent(e, span:CalSpanThisEvent, error:errorPointer)
puts "Error: #{errorPointer[0].userInfo}"if !success
 
puts "Finished calendar macruby test"

In the first step we have to get the default CalCalendarStore. This object contains all calendars we have configured in iCal.

The next few lines query the calendar which the test event should be put in.

From line 17. on the CalEvent object is initialized with several values. Note, that the chosen calendar is a property of the event. In this test case the event is created at now + 2hours end in now + 4hours. You can see that Ruby data types blend wonderfully into Objective-C types, as usually startDate and endDate would need a NSDate object.

For the perfect topping an alarm is created from line 23. on. The CalAlarm object is initialized with several values and then added to the event. You could go forward and start to add more reminders, as e.addAlarm a can be called multiple times.

Until now the stuff is not yet saved. Apple designed the library so that the CalCalendarStore object is responsible to save the event. So we have to call defStore.saveEvent, with the event object itself, the span (relevant for recurring events), and an error pointer.

If you did everything correct, you should see a new event in your iCal!

Writing a jQuery plugin with coffeescript

Coffescript saves my day, as I don’t like javascript and the coffeescript syntax is more python/ruby based. Coffeescript itself compiles to javascript. It is very easy to learn, nevertheless it was not easy to create the basic code to register a new jQuery plugin. The following code serves as template:

1
2
3
4
5
6
7
8
9
10
11
$ = jQuery
 
methods =
  init: (options) ->
    obj = @
    # do something
 
$.fn.your_plugin_name = (method) ->
  if method of methods
    return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
  else $.error( "Method #{method} does not exist on jQuery.your_plugin_name" );

This coffeescript compiles to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var $, methods;
$ = jQuery;
methods = {
  init: function(options) {
    var obj;
    return obj = this;
  }
};
$.fn.your_plugin_name = function(method) {
  if (method in methods) {
    return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
  } else {
    return $.error("Method " + method + " does not exist on jQuery.your_plugin_name");
  }
};

You would call your plugin e.g. this way (from javascript):

$('#target_div').your_plugin_name('init', {your: "option"});

If you are still waiting for the *Yeah, I have to use coffeescript!”-moment read this:

The lines of code of my jQuery plugin were reduced by 50% and the code is much more readable now! Don’t miss the coffeescript page which will give you much more arguments for coffeescript as I could ever do here.

Uninstall all local ruby gems

This ruby script might be useful for people like me who just started using bundler to manage the gems of their webapp and all others, which want to clean up their gems. Caution: all installed gems are removed from rubygems!

1
2
3
4
5
6
7
f = open("|gem list")
output = f.read()
 
output.each_line do |l|
	gem_name = l.split(" ").first
	system "gem uninstall --a --ignore-dependencies #{gem_name}"
end

Basically this script just executes the “gem list” command, fetches the output, extracts the gem name and uninstalls it.

Rake & FTP

Rake is a build tool for ruby, similar to C’s make or Java’s ant/maven.

Basically you can define a lot of tasks, which can be executed, may have prerequisites and can do a lot boring tasks, from file operations to building gems.

One great thing is that you can easily deploy your whole project to your FTP server. Searching around I did not find a lot of information about that – this is the reason I’ll write about it.

Rake::FtpUploader
Rake has a built in class for handling simple FTP uploads, but this class is not well documented. You can use it this way:

1
2
3
4
5
6
7
8
9
require 'rake'
require 'rake/contrib/ftptools'
 
task :upload_all do
  Rake::FtpUploader.connect('/path/on/server', 'your_host', 'your_user', 'your_pw') do |ftp|
    ftp.verbose = true # gives you some output
    ftp.upload_files("./your/favorite/folder/**/*")
  end
end

The ftp.upload_files method takes a wildcard parameter, which is the same as you would feed into the Dir[wildcard] class. At the end, the FtpTools just use Dir to find all files. For the wildcard it is good to know, that a single * says “upload all files and folders”, but a **/* says “upload all files and folders in a recursive way, including subfolders and their files. Thus said, “./**/*” would upload everything from the base dir of your rakefile.

Extending Rake’s FtpTools for deletion
In my case I was using rake to upload files for a PHP project – written with the symfony framework. Symfony – as many other frameworks – has a cache which needs to be cleared if some settings for the view change. As the cache is simply a folder on the FTP server, I thought that rake could be used to easily clear the cache. Reminding myself of the fact that everything in Ruby is just an object, I just extended the FtpUploader class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
module Rake
  class FtpUploader
 
    # Deletes all files in a folder like cache/management. Does
    # not delete folders
    def delete_files(folder)
      delete_files_recursive(folder)
    end
 
    private
 
    # starts with a folder, like cache/management and deletes
    # all files recursively BUT not the folders.
    def delete_files_recursive(file_or_folder)
      folder = true
      begin
        @ftp.chdir(file_or_folder)
      rescue
        # this is a file, no folder => delete
        folder = false
      end
 
      if folder
        file_list = @ftp.nlst('*')
        file_list.each { |f| delete_files_recursive(f) }
        @ftp.chdir('..')
      else
        puts "Delete #{file_or_folder}" if @verbose
        delete file_or_folder
      end
 
    end
 
    # deletes a single file
    def delete(file)
      puts "Deleting #{file}" if @verbose
 
      begin
        @ftp.delete(file)
      rescue
        puts "Could not delete file #{file}"
      end
    end
  end
end

This snippet only deletes files, no folders. In my case this is wanted, as I’m not sure what the cache does if you destroy the folder structure, but nevertheless it could be easily extended. After all files of a folder are deleted, after the @ftp.chdir(‘..’) is executed, the folder could be deleted.
Usage

1
2
3
4
5
6
7
namespace :cache do
  task :clean
    Rake::FtpUploader.connect('/path/on/server', 'your_host', 'your_user', 'your_pw') do |ftp|
      ftp.delete_files('cache/subfolder')
    end
  end
end

Ruby web scraping

I want to share my recent experience with ruby web scraping and RSS.

Often I found myself checking the comments for my game iBox3D using http://www.appcomments.com. But the problem is that you can only display the comments by country and my app does so far only have comments in Germany, USA and UK. But the website does not show where you have comments or not. It was hard to say, if there were new comments or not. So I decided to write a small ruby script which should compile all comments and display them. The script evolved as I continued to optimize it:

  • v1: I used WATIR to automate the internet explorer and to get the HTML from the website
  • v2: I used the built in RSS lib to fetch the RSS
  • v3: I used simple-rss to fetch the RSS
  • v4: I used simple-rss to fetch the RSS and used threads to do that for all countries simultaneously

This post will handle following topics:

  • Ruby 1.9
  • Watir
  • RSS with the build-in RSS lib and simple-rss
  • utf8 vs. ASCII
  • Threading
  • JRuby and Ruby

Version 1: Watir

Watir is a lib that helps to automate the browser. For those who want to install Watir at Ruby 1.9 you need to install the ruby devtools and then install the gem with the –platform=ruby option. Here is the script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
require 'watir'
 
### hide or show the internet explorer
$HIDE_IE = true
 
# declare some variables
source_url = "http://appcomments.com/app/iBox3D"
comments = Hash.new
country_links = Array.new
 
### open the internet explorer and navigate to the source url
ie = Watir::IE.new()
ie.goto(source_url)
 
### fetch the links to the different countries
ie.links.each do |link|
	if link.href =~ /iBox3D\?country=(\d*)$/
		country_links < < link.href
	end
end
 
### loop over all country links
country_links.each do |link|
	# go to the country specific link
	ie.goto(link)
 
	# due to unknown reasons sometimes the DIV was not there => reload
	until  ie.div(:id => "review_dropdowns").exists? do
		sleep(1)
		ie.goto(link)
		sleep(5)
	end
 
	# get the DIV with id = review_dropdowns
	review = ie.div(:id => "review_dropdowns")
	# get the country text from the SPAN in the DIV
	country = review.divs.first.spans.first.text
	# loop over all DIVs
	ie.divs.each do |d|
		# check the DIV class, if it has "comment", then proceed
		if d.class_name.downcase.match(/\bcomment\b/i)
			# initialize a hash for the comment information
			inf = Hash.new
			# the first link in the div is the title/header
			inf[:header] = d.links[1].text
			# the second link in the div is the user
			inf[:user] = d.links[2].text				
			# loop over all DIVs in the DIV and find the comment_right class (rating)
			d.divs.each do |star_div|
				if star_div.class_name.downcase.match(/\bcomment_right\b/i)
					# count the start image tags
					counter = 0
					star_div.images.each do |img|
						counter += 1
					end
					inf[:stars] = counter
				end
			end
			# get the description of the rating
			inf[:text] = d.ps.first.text
			# store the information in the comments hash
			comments[country] = inf
		end
	end
end
 
### show the result
puts comments.inspect

To understand the coding I suggest that you use Firebug or the Internet Explorer Developer Tools to have a look at the HTML of the coding of http://appcomments.com/app/iBox3D. The comments should be enough to understand the coding – at least I hope so. Nevertheless I want to tell you why I did not stop here and continued to search for alternatives:

  • if the html structure changes, the script will not work any more (no interface contract)
  • it takes very long to start of the internet explorer and to visit each page
  • this script is not platform independent, for other platforms you would have to use e.g. firewatir
  • if I would use threads for parallelisation it would be very memory intense, because many IE instances take a lot memory

Then I noticed the RSS feed on appcomments.com.

Version 2: Ruby and RSS

RSS in ruby should be simple:

1
2
3
4
5
6
7
8
9
10
require 'rss/1.0'
require 'rss/2.0'
require 'open-uri'
 
source = 'http://appcomments.com/rss/376860218?country=143443'
 
content = "" # raw content of rss feed will be loaded here
open(source, :proxy => "http://proxy:8080") do |s| content = s.read end
rss = RSS::Parser.parse(content, false)
[...]

But try to run it! It will fail with the error:


C:/dev/runtime/Ruby191/lib/ruby/1.9.1/rss/rexmlparser.rb:24:in `rescue in _parse': This is not well formed XML (RSS::NotWellFormedError)
#encoding ::CompatibilityError: incompatible encoding regexp match (UTF-8 regexp with ASCII-8BIT string)
[...]
Exception parsing
Line: 12
Position: 462
Last 80 unconsumed characters:
< ![CDATA[Brilliantes Spiel fürs iPhone]]>
[...]

German umlauts! (ä,ö,ü) Somehow ruby 1.9, which claims to have UTF-8 support, introduces a lot problems. The german character ü cannot be parsed by the internal rexml. I found the simple solution to force ruby to think that the content string is utf-8, using the force_encoding method. Then parsing works.

1
2
3
4
5
6
7
8
9
10
require 'rss/1.0'
require 'rss/2.0'
require 'open-uri'
 
source = 'http://appcomments.com/rss/376860218?country=143443'
 
content = "" # raw content of rss feed will be loaded here
open(source, :proxy => "http://proxy:8080") do |s| content = s.read end
content.force_encoding('utf-8')
rss = RSS::Parser.parse(content, false)

Version 3: Simple-RSS

Before I found that out I tried the ruby lib simple-rss, which could parse the ü (german umlaut). Nevertheless I had to do the same trick as above when I wanted to access the parsed content. At this point I want to introduce the next evolution step of my script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
require 'rubygems'
require 'simple-rss'
require 'open-uri'
 
$base_html = < <EOF
<?xml version="1.0" encoding="utf-8"?>
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>@@title</title>
</head>
<body>
EOF
 
$base_html_2 = < <EOF
</body>
</body></html>
EOF
 
def get_content(url)
	content = String.new
	open(url, :proxy => "http://proxy:8080") do |s| 
		content = s.read end
	return content
end
 
def utf8(string)
	return string.force_encoding('utf-8')
end
 
source = "http://appcomments.com/app/iBox3D"
countries = get_content(source).scan(/a href='\?country=(\d*)'.+?>(.+?) "http://proxy:8080")
 
	next if rss.items.size == 0
	html < < "<hr/><h1>#{country[1]}</h1>" 
	rss.items.each do |i|
		html < < "<div><h3>#{utf8 i.title}</h3><p>#{utf8 i.description}</p>"
	end
end
 
### write everything to a file
html < < $base_html_2
local_filename = "appcomments.html"
File.open(local_filename, 'w:utf-8') do |f|
	f.write(html)
end

Besides the advantages listet below I also added a file output to an HTML file. This is because the RSS description tag contains HTML which can be easily dropped to a HTML file.

Advantages:

  • RSS is a standardized protocoll, so the structure won’t change in future (interface contract)
  • instead of opening the browser to perform the scraping open-uri is used, which is faster and not so memory consuming
  • this script should run on many platforms, including linux, mac os x and windows
  • the process of making the web request is way easier

Version 4: Threading

Still my script took very long. No wonder, it had to make a GET request per country and one additional for the overview site to get all the country codes. But using ruby built-in threads it is easy to make all requests parallel! This speeds up the whole script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
require 'rubygems'
require 'simple-rss'
require 'open-uri'
 
### define the HTML basis
$base_html = < <EOF
<?xml version="1.0" encoding="utf-8"?>
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>@@title</title>
</head>
<body>
EOF
 
$base_html_2 = < <EOF
</body>
</body></html>
EOF
 
### get request to a URL
def get_content(url)
	content = String.new
	open(url, :proxy => "http://proxy:8080") do |s| 
		content = s.read end
	return content
end
 
### forces the encoding in UTF-8
def utf8(string)
	RUBY_PLATFORM == 'java' ? string : string.force_encoding('utf-8')
end
 
### define the basic URLs and the filename
source_url = "http://appcomments.com/app/iBox3D"
rss_url = "http://appcomments.com/rss/376860218?country="
local_filename = "appcomments.html"
 
### get the main page to get all countries
countries = get_content(source_url).scan(/a href='\?country=(\d*)'.+?>(.+?)
threads = []
countries.each do |country|	
	threads < < Thread.new do
		# construct the URL with the number of the country
		url = rss_url + country[0]
		# get the RSS feed from the URL
		rss = SimpleRSS.parse open(url, :proxy => "http://proxy:8080")
		# construct the HTML with the country information and the review
		country_html = String.new		
		# go to the next country if there are no reviews
		next if rss.items.size == 0
		# construct the country header
		country_html < < "<hr/><h1>#{country[1]}</h1>" 
		# construct a div for each review
		rss.items.each do |i|
			country_html < < "<div><h3>#{utf8 i.title}</h3><p>#{utf8 i.description}</p>"
		end
		# set the thread variable for later access
		Thread.current["html"] = country_html
	end
end
 
### join the threads and construct the HTML
threads.each do |t|
	# join the threads
	t.join
	# construct one big HTML chunk out of the small HTML junks
	html < < t["html"] unless t["html"].nil?
end
 
### write everything to a file
html << $base_html_2
File.open(local_filename, 'w:utf-8') do |f|
	f.write(html)
end

By the way, this code works also with JRuby, and the only source I had to adjust was the following line:

1
RUBY_PLATFORM == 'java' ? string : string.force_encoding('utf-8')

JRuby is able to handle the utf-8 format way better.

iBox3D released

Back in 2009, on the 23rd of March, I’ve reported about an easy way to created 3D iPhone games with Shiva, a game creation suite from Stonetrip.

Soon it turned out that it was easy to setup a quick prototype which contains only a subset of functionality of the final version. I’ve worked on iBox3D in my spare time as often as possible, but also I had to take some creative breaks. The more functionality I’ve built in the game the harder the work was getting. I’ve learned that a finished and polished product needs a lot of time, more that anyone would estimate. But after I’ve set a clear target to myself I could concentrate to get there. And finally I’ve managed to create a game for iPhone / iPod Touch and iPad.

iBox3D can be downloaded for € 0,79 / $ 0,99 in the Apple Appstore.

Also the game has now its own website which can be found here:

http://www.ibox3d.com

I appreciate your feedback.

Ruby on Windows: OLE automation for Outlook – Shared calendars

Ruby has a nice library for OLE automation, win32ole. I used this library to get appointments from shared outlook (exchange) calendars. The scripts are testen on Windows Vista with Outlook 2003. This is how it works:

outlook_reader.rb

require 'win32ole'
outlook = WIN32OLE.new('Outlook.Application')
mapi = outlook.GetNameSpace('MAPI')

# this is explained later
Thread.new do
  system('outlook_clicker.exe')
end

# create a recipient and resolve him
myRecipient = mapi.CreateRecipient("John Doe")
myRecipient.Resolve

# get the shared calendar of the recipient
if myRecipient.Resolved
  calendar = mapi.GetSharedDefaultFolder(myRecipient, 9)
end

# write the calendar data to the output
if calendar
  calendar.Items.each do |item|
    puts "#{item.Start} - #{item.End}: #{item.Subject}"
  end
end

Outlook has a mechanism which should protect the outlook data from unauthorized access, e.g. viruses and trojans. Everytime you want to access the data from outlook a popup opens which asks you, if the access should be granted. In case of automation this is very bad, because you usually don’t want to click anything if you automate a process.

So here the solution. I’ve created a ruby script which clicks the dialog for the user. Then I converted it into an executeable using RubyScript2exe (Ruby code gets compiled).

outlook_clicker.rb

require 'win32ole'

# new shell object
wsh = WIN32OLE.new('Wscript.Shell')

# for timeout management
time_out = 0
time_out_max = 10
interval = 0.01

# try to activate the window with the Title 'Microsoft Office Outlook'
while !wsh.AppActivate('Microsoft Office Outlook') do
  time_out = time_out + interval
  sleep(interval)
  if time_out > time_out_max
    Process.exit!
  end
end

# if the popup exists and the timeout is not reached some
# keys get send to the popup
wsh.SendKeys("{TAB}")
wsh.SendKeys("{TAB}")
wsh.SendKeys(" ") # this is "space"
wsh.SendKeys("{TAB}")
wsh.SendKeys("{TAB}")
wsh.SendKeys("{ENTER}")

To compile this ruby file into an executeable rubyscript2exe can be used. To install it you can use Ruby Gems (command prompt):

gem install rubyscript2exe

After installing you can open a command prompt, change to the directory your scripts are in and compile the clicker using the following command

ruby rubyscript2exe.rb outlook_clicker.rb

The executable will be created in the same directory. In the file outlook_reader.rb the following lines call the outlook_clicker.exe:

Thread.new do
  system('outlook_clicker.exe')
end

In this three lines a new thread is created which calls the outlook_clicker.exe using the system method. Basically we reached that both scripts are executed in parallel.

If you execute the outlook_reader.rb you can see that the popup shortly opens and then automatically gets closed.

Recommend reading: http://rubyonwindows.blogspot.com/