Creating a Twitter Client for OSX in MacRuby - Part 2

Posted in Development on Sunday, Sunday, April 26, 2009 by Anthony Burns

UPDATE: Part Three of this series is now available

Adding Basic Authentication

Adding Basic Authentication to a url request is pretty straight forward, in fact it only requires a header to be added to the request with the NSURLRequest.addValue:forHTTPHeaderField method. Simple huh? Not quite. The HTTP spec requires that the username and password be Base64 encoded, which is usually a case of requiring the base64 library in standard Ruby, but that isn't available in MacRuby, and there doesn't appear to be Cocoa object designed for the purpose.

After spending five minutes googling turned up exactly bot all, I decided to change tack and find out how to base64 encode myself.

How to Base64 Encode
http://email.about.com/cs/standards/a/base64_encoding.htm

So with a combination of the above article, a trusty Ruby book, blood, sweat, tears and a lot of trial and error I constructed:

class Base64

	ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-="

	def encode(str)
	
		dat = []
		str.each_byte { |b| dat << b }
		
		# Append 0s to the data to make it's length divisible by 3
		missingChars =  3 - (dat.length % 3)		
		missingChars.times { dat << 0 }
		
		result = ''
		sections = dat.length / 3
		sections.times do |i|
		
			a = dat[(i * 3) + 0] >> 2
			b = dat[(i * 3) + 0] << 4 & 63
			b = b | dat[(i * 3) + 1] >> 4
			c = dat[(i * 3) + 1] << 2 & 63 
			c = c | dat[(i * 3) + 2] >> 6
			d = dat[(i * 3) + 2] & 63
			
			if dat[(i * 3) + 1] == 0 then c = 64 end
			if dat[(i * 3) + 2] == 0 then d = 64 end
		
			result += ALPHA[a] + ALPHA[b] + ALPHA[c] + ALPHA[d]

		end

		return result
	end

end

There's no point me stepping through it, you should be able to dissect it easy enough with the referenced article, or just use it if you don't care how it works - it's not perfect and probably won't work in all scenarios, but it's good enough for Base64 encoding a username/password combination.

We can then use the Base64 class to encode our credentials and add the Basic Authentication header, which I've chosen to do in a separate method of the TwitterApi class called addAuthentication:

def addAuthentication(req)
	enc = Base64.new
	data = @username + ':' + @password
	data = enc.encode(data)
	data = 'Basic ' + data
	
	req.addValue data, forHTTPHeaderField:'Authorization'
end

I've also added a couple of attributes to the class that allow the username and password to be set:

attr_accessor :username, :password

And I've added a call to addAuthentication to the getUrl method as well as an authenticated parameter to the method signature, allowing you to specify if you want the request to be authenticated or not:

	def getUrl(urlString, authenticated, delegate)

		callback = HttpRequestCallback.new
		callback.delegate = delegate
		callback.buf = NSMutableData.new
		callback.response = nil

		url = NSURL.URLWithString(urlString)
		request = NSMutableURLRequest.requestWithURL(url, cachePolicy:POLICY, timeoutInterval:TIMEOUT)

		addAuthentication(req) if authenticated # <-- Add Authentication if required

		callback.conn = NSURLConnection.alloc.initWithRequest(request, delegate:callback)

	end

Finally I've added a getTimeline method to the class that calls getUrl for the authenticated user's timeline:

def getTimeline(callback)

	getUrl('http://twitter.com/statuses/friends_timeline.xml', true, lambda {|data| callback.call(data) })

end

We can consume this like so:

twitter = TwitterApi.new
twitter.username = 'xxx'
twitter.password = 'xxx'
twitter.getTimeline(lambda do |data|

	result = NSString.alloc.initWithData data, encoding:NSUTF8StringEncoding

	# display the result in an alert box
	alert = NSAlert.new
	alert.setMessageText(result)
	alert.runModal()	

end)

Which should download the Xml of your authenticated Twitter timeline and display it in a message box.

So we can now obtain our own timeline authenticated using Basic Authentication; next we'll parse the Xml into an array of status objects and bind them to the table in our main window. Part Three Available Here.

Tagged as: cocoa, macruby, ruby

Comments

Luc Heinrich spoke on Sunday, Sunday, April 26, 2009

MacRuby doesn't have the base64 library because it is useless. You can encode any string in a single line by doing: [str].pack("m") For example: ["Hello World"].pack("m") = "SGVsbG8gV29ybGQ=\n"

Burns spoke on Monday, Monday, April 27, 2009

Thanks for the pointer Luc, but it doesn't seem to work for me - pack("m") returns an empty string.

Pete spoke on Monday, Monday, April 27, 2009

Quote: "I decided to change tack and find out how to base64 encode myself" ...sweet! Does that mean you can transmit yourself to anywhere that can decode base64? :-)

Luc Heinrich spoke on Tuesday, Tuesday, April 28, 2009

Hmm, maybe try .pack("m*") (with the *)?

Keenan Brock spoke on Sunday, Sunday, May 03, 2009

[@username + ':' + @password].pack("m").chomp the chomp was eluding me. With it makes it work. printing the 2 out sometimes hides the new line.

Burns spoke on Wednesday, Wednesday, May 06, 2009

@Luc + @Keenan I've tried both of those and neither seems to work. I have a method called alert(msg) that pops up an NSAlert box containing the msg, and I've tried: alert(['mystring'].pack("m*")) as well as the chomp one and the alert always comes up empty.