Creating A Custom Image Upload Service For Tweetie (Twitter For iPhone) Using ColdFusion
The other day, I was uploading a photo from Tweetie (Twitter for iPhone) when it occurred to me that I might be able to write my own custom image upload service using ColdFusion. After all, if Tweetie supports multiple image upload services, then I figured all I would have to do is follow some standard API to make it work. As it turns out, this is exactly correct. If you look in the Services section of your Tweetie settings, it allows you to define a custom image upload service:

This setting is nothing more than a single web service URL that supports a common API. According to the documentation on the AteBits development site, the web service API to model is the TwitPic image upload API. From what I can tell, though, the API supported by Tweetie is a subset of the image upload functionality provided by TwitPic.
Since Tweetie was bought by Twitter, the documentation on AteBits has become outdated. Tweetie no longer posts the Twitter username and password of the individual uploading the given photo. After a good deal of Googling, I finally figured out that Tweetie is now taking an "oAuth Echo" approach to user validation. In this approach, the client application - Tweetie - secures oAuth credentials for the user. It then posts the image along with both the oAuth credentials and a user validation web service URL to the 3rd party application - our image uploader. Our image uploader then uses both the oAuth credentials and the user validation web service URL to validate the user. Once the user is validated, we can then store the uploaded image and return our XML web service response to the client application (Tweetie).

Once the 3rd party application has secured the oAuth credentials, it posts the following HTTP headers along with the image upload:
- X-Auth-Service-Provider
- X-Verify-Credentials-Authorization
The "X-Auth-Service-Provider" header provides a URL to the user validation service that our image uploader is supposed to use to validate the user (posting the image). In this case, is posts the following Twitter API url:
https://api.twitter.com/1/account/verify_credentials.json
The "X-Verify-Credentials-Authorization" header provides the oAuth credentials that we are supposed to post the above validation web service. This value will look something like this:
OAuth oauth_timestamp="1279135448", oauth_version="1.0", oauth_consumer_key="IQKbtAYlXMLDipLGPWd0HUA", oauth_token="15148392-0X4TUDVS1137rkucSCoxLMgtPhGldnHpq02v", oauth_signature="VwqniHwBe%2BdangTastydYUGj3BTxz5c%3D", oauth_nonce="7C7D01FD-2777-FFC0-802C-087077ACABC6", oauth_signature_method="HMAC-SHA1"
This oAuth value needs to be posted as the "Authorization" header to the validation web service. The validation web service will either respond with a 401 Unauthorized response, or it will respond with a 200 OK response. If it returns with a 200 OK response, then the content of the HTTP response will be a JSON representation of the authorized Twitter user. It is from this JSON structure that we can get the username of the Twitter user that is uploading the given image.
Understanding the oAuth Echo work flow was the biggest hurdle in creating a custom ColdFusion image upload service for Tweetie. Once I figure it out, however, the rest was little more a standard file upload. According to the TwitPic API, you are supposed to return different error codes based on what aspect of the image upload algorithm failed (authorization, file size, etc.); but, for this demo, I am keeping it very simple - it either works or it doesn't. I'm not bothering to break down the exception reasons, although, you'll see in the following code that I am raising different exceptions which can be caught and handled individually if you like.
That said, here is the ColdFusion code that handles the image upload from Tweetie. It is this file that is defined as the end point in the custom upload service setting.
Tweetie Custom Image Upload Service
<!--- | |
Define the uploads directory - this is where the images | |
will be stored after the user has been authorized. | |
---> | |
<cfset uploadDirectory = expandPath( "./uploads/" ) /> | |
<!--- | |
Define the uploads URL. This is the externally-accessible URL | |
to the uploads directory. When we respond to this web service | |
call, we have to return a media URL. | |
---> | |
<cfset uploadUrl = ( | |
"http://" & | |
cgi.server_name & | |
getDirectoryFromPath( cgi.script_name ) & | |
"uploads/" | |
) /> | |
<!--- | |
Wrap a Try/Catch around the processing since we might | |
break something and we need to be able to send back a | |
failure response. | |
---> | |
<cftry> | |
<!--- Check to make sure that the request is a POST. ---> | |
<cfif (cgi.request_method neq "post")> | |
<!--- Throw error - we only accept POST. ---> | |
<cfthrow | |
type="InvalidMethod" | |
message="Only POST is supported." | |
/> | |
</cfif> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- Param the form parameters. ---> | |
<!--- | |
This is the form field that contains the image data. This | |
image data is coming across in the same way a standard | |
File/Upload works. | |
---> | |
<cfparam name="form.media" type="string" /> | |
<!--- | |
This is the twitter message associated with the image being | |
uploaded. We will not be making use of it in this demo, but | |
you can certainly use it. | |
---> | |
<cfparam name="form.message" type="string" default="" /> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- | |
Get the HTTP request header data. We will make repeated use | |
of this, so let's just get a short-hand reference to it. | |
---> | |
<cfset httpHeaders = getHttpRequestData().headers /> | |
<!--- | |
Check to make sure that the Twitter client has passed | |
along the appropriate headers with the request. We are going | |
to be using an "oAuth Echo" approach which means the client | |
will be passing us the oAuth information as well as a service | |
we need to use to verify the credentials. | |
---> | |
<cfif !( | |
structKeyExists( httpHeaders, "X-Auth-Service-Provider" ) && | |
structKeyExists( httpHeaders, "X-Verify-Credentials-Authorization" ) | |
)> | |
<!--- Throw error - we are missing authorization headers. ---> | |
<cfthrow | |
type="Unauthorized" | |
message="oAuth Echo authorization headers were not supplied." | |
/> | |
</cfif> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- | |
Since we have the authorization headers for the oAuth Echo | |
approach, let's validate the user's credentials with the | |
Twitter service. We are hoping for a 200 response with the | |
Twitter USER data. | |
NOTE: We are going to assume that this is coming back in JSON | |
format. We could check the URL, but we'll just go with it | |
for the moment. | |
---> | |
<cfhttp | |
result="oAuthEcho" | |
method="get" | |
url="#httpHeaders[ 'X-Auth-Service-Provider' ]#"> | |
<!--- Pass along the oAuth information. ---> | |
<cfhttpparam | |
type="header" | |
name="Authorization" | |
value="#httpHeaders[ 'X-Verify-Credentials-Authorization' ]#" | |
/> | |
</cfhttp> | |
<!--- | |
Check to make sure that we got a 200 response. If we didn't, | |
then something went wrong - either bad credentials or the | |
timestamp of the oAuth signature was no longer valid. | |
---> | |
<cfif !findNoCase( "200", oAuthEcho.statusCode )> | |
<!--- Throw error - Twitter authorization failed. ---> | |
<cfthrow | |
type="Unauthorized" | |
message="Twitter authorization failed." | |
/> | |
</cfif> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- | |
Since authorization worked, let's parse the Twitter response. | |
It should contain the JSON data for the authorized user. | |
---> | |
<cfset user = deserializeJSON( | |
toString( oAuthEcho.fileContent ) | |
) /> | |
<!--- | |
Twitter may have authorized this user, but let's make sure | |
that this user is "internally" authorized to use this image | |
upload service. For this demo, only Ben can upload. | |
---> | |
<cfif (user.screen_name neq "bennadel")> | |
<!--- Throw error - user is not internally authorized. ---> | |
<cfthrow | |
type="Unauthorized" | |
message="Internal authorization failed." | |
/> | |
</cfif> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- | |
If we've made it this far, then the user is authorized. Let's | |
upload the photo. The Image data is posted in the form in the | |
"media" form field. | |
---> | |
<cffile | |
result="upload" | |
action="upload" | |
filefield="media" | |
destination="#uploadDirectory#" | |
nameconflict="makeunique" | |
/> | |
<!--- Make sure the file type is acceptable. ---> | |
<cfif !listFindNoCase( "jpg,gif,png", upload.serverFileExt )> | |
<!--- The file is no good, delete it. ---> | |
<cffile | |
action="delete" | |
file="#uploadDirectory##upload.serverFile#" | |
/> | |
<!--- Throw an error - file is not a valid type. ---> | |
<cfthrow | |
type="InvalidFileType" | |
message="Only files with JPG, GIF, or PNG extensions are supported." | |
/> | |
</cfif> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- | |
If we've made it this far, then the user has been | |
authorized and the image has been SUCCESSFULY uploaded. | |
Let's return a success response. | |
At this point, you'd probably want to actually store the | |
image upload record in a database and return something like | |
a Unique ID or short URL to a "view page". But, to keep | |
this demo super simple, I am just returning the physical | |
image resource url. | |
NOTE: The FORM scope also has a "message" field, which is the | |
twitter message accompanied with the image. We are not making | |
any use of this field at this sime. | |
NOTE: Tweetie (Twitter for iPhone) doesn't support the | |
MediaID node in our response; but, TwitPic uses it, and | |
TwitPic is the API we are supposed to model so I am just | |
throwing it in here. | |
---> | |
<cfsavecontent variable="response"> | |
<cfoutput> | |
<?xml version="1.0" encoding="UTF-8"?> | |
<rsp stat="ok"> | |
<mediaid>#getTickCount()#</mediaid> | |
<mediaurl>#uploadUrl##upload.serverFile#</mediaurl> | |
</rsp> | |
</cfoutput> | |
</cfsavecontent> | |
<!--- ------------------------------------------------- ---> | |
<!--- ------------------------------------------------- ---> | |
<!--- | |
Catch any raised exceptions. In the above work flow, we have | |
been throwing typed-errors. This gives us the ability to | |
catch specific errors; but, for this demo, I am only catching | |
general errors. If you wanted to add some CFCatch/Type tags, | |
you'd be able to better support the TwitPic error response | |
specifications for error codes (see "err" XML node below): | |
1001 - Invalid twitter username or passwor | |
1002 - Image not found | |
1003 - Invalid image type | |
1004 - Image larger than 4MB | |
For now, I'm just returning error "2001" since I am not | |
mapping exceptions to valid error types. | |
---> | |
<cfcatch> | |
<!--- | |
If anything went wrong, try to delete the temporary file | |
from the implicit ColdFusion upload directory. This gets | |
cleared out periodically, but let's help the cause. | |
The file path to the .TMP file is stored in the media | |
form field. | |
---> | |
<cftry> | |
<!--- Delete the TMP file from the form post. ---> | |
<cffile | |
action="delete" | |
file="#form.media#" | |
/> | |
<cfcatch> | |
<!--- | |
The temp file could not be deleted. Perhaps it | |
has already been (moved and) deleted by a step | |
above. | |
---> | |
</cfcatch> | |
</cftry> | |
<!--- | |
Define the error response. Since we are creating XML, | |
be sure to escape the dynamic text in case it has any | |
non-XML safe characters. | |
---> | |
<cfsavecontent variable="response"> | |
<cfoutput> | |
<?xml version="1.0" encoding="UTF-8"?> | |
<rsp stat="fail"> | |
<err code="2001" msg="Unknown error - #xmlFormat( cfcatch.message )#" /> | |
</rsp> | |
</cfoutput> | |
</cfsavecontent> | |
</cfcatch> | |
</cftry> | |
<!--- ----------------------------------------------------- ---> | |
<!--- ----------------------------------------------------- ---> | |
<!--- Stream the XML response back to the client. ---> | |
<cfcontent | |
type="text/xml" | |
variable="#toBinary( toBase64( trim( response ) ) )#" | |
/> |
Because there are so many dependent steps in this work flow that might break, I am using a CFTry / CFCatch / CFThrow approach (as outlined in my ColdFusion API Presentation). In using this approach, not only can I easily respond to individual exceptions using typed-CFCatch tags, I can also be sure that at every step along the way, all of the previous steps were valid.
To keep this demo as simple as possible, I am returning the physical resource URL of the uploaded image. In reality, you'd probably want to take the uploaded URL, insert it into a database, and create some sort "view" page for said image. Then, you'd probably want to return a short-url to said view page in lieu of returning the actual image URL. But, at that point, we would need to get much more specific in our implementation. This code is meant to be a general approach to handling custom image uploads from Tweetie.
Want to use code from this post? Check out the license.
Reader Comments
Thanks I was looking for something like tweetie, for a project I'm considering for http://redtideflorida.org/page/ where people could upload photos and tweet to help track the oil damage on beaches (and red tide when it comes back) in Florida.
But hey, maybe a little fairy will come, wave her wand and make this service not necessary!
BTW, your code above is freakin beautiful.
@Texxs,
Thanks my man. Good luck with your app - sounds like a very environmentally cool service.