Sunday, May 30, 2010

Building Wave Robots in ColdFusion with OpenBD on Google App Engine

Robot development for Google Wave has sparked my imagination lately. You can* write Robots in Java and Python and run them on Google App Engine, but what about ColdFusion ? Well since Open Blue Dragon (OpenBD) can now run on GAE, I thought it would be fun to have a go at a Wave Robot written in ColdFusion.

 

Update: 1 Jun 2010 - Its working now (Yea !).

I have working Operations being sent back to the server causing changes in the Wave. The profile is also working. The problem was :

  • The JSON that the wave server is expecting is case sensitive. Key names like childBlipIds which need to be in the precise camel-case.

    I had to use a 3rd party CFJSON serializer because the OpenBD built-in one forces upper or lower case key names.

  • Adding keys to objects you are creating to send back to the wave server need to be the correct case, using the correct method.
    • This is WRONG :
      • o.params = StructNew();
      • o.params.blipData = NewBlip(); // Case of the keyname will change to PARAMS and BLIPDATA
    • This is CORRECT:
      • o['params'] = StructNew();

      • o['params']['blipData '] = NewBlip();

    • So make sure you create your keys using the [‘keyName’] notation so that the case is preserved

 

Try it yourself. Invite cfwavebot@appspot.com to a wave.

 

 

image

After reading  API docs for the Google Wave, the  Robot Wire Protocol is what I decided to implement (partially – as a proof or concept). It is a light-weight message and event protocol that uses JSON to exchange data.

 

*Update : At I/O 2010 Google announced that Robots can run on servers other that Google App Engine. I haven't tried it myself yet.

 

This blog post is about my efforts so far, and explains how to get setup the way I did. CFML source code is included

 

In this article :

  • What is / isnt working ?
  • Problems Overcome
  • What I have / what remains to be implemented
  • How To Build Your Own ColdFusion Wave Robots
  • Essential Reading
  • Where to get help

 

clip_image002

 

 

What is / isnt working ?

  • jsonrpc message bundles are being received by the robot OK
  • The robot is replying with operations json :
    • wavelet.appendBlip
  • Fixed ! The Wave Server wont:
      • process the operations json the robot sends back
      • Display any of the profile details (avatar, profile web link)

 

You can see how it is currently running from this screencast (doesn’t have sound).

http://www.youtube.com/watch?v=q3h4oU7hGKI

Notice from the video that :

  • I start a new wave with the robot but nothing from it  appears in the wave
  • From the GAE logs, you can see the robot receives  jsonrpc messages and responds with operations like "wavelet_appendBlip"
  • I conclude (with a guess) that the UPPERCASE field names that ColdFusion creates when using its serializejson() function cant be understood by the Google Wave Server

There are more detailed screenshots of the issues so far below.

 

Problems Overcome

 

  • Logging to the console is the only way to debug. Forget about <cfdumps> to the page, because you are always returning json strings, not HTML. Fortunately good-ole <cflog > works both locally and on Google App Engine.
  • Debugging with a locally running version of your robot is much quicker that deploying to GAE. You need to fake jsonrpc calls from the Wave Server which you can do with a simple web page that you can cut and paste json messages and then fire them at your robot.
  • Different JSON http content encoding (binary and text)
  • What isn't documented
  • OpenBD can't process requests like /robots/jsonrpc without a URL re-writer
    • You need to use the tuckey url rewriter

 

How the ColdFusion version works

 

I started with a single cfml template for each of the 3 URLs that the Robot must answer on. ColdFusion has built-in support for JSON serialization (although its insistance on CAPITALIZING field names has be stuck at the moment).

 

I made use of the URL rewriting technique for OpenBD described on the OpenBD wiki to handle all the required Robot URLS :

 

  • URL re-writing maps the required calls as follows :

Robot URL

Mapped to CFML Script

_wave/capabilities.xml

_wave/robot/capabilies.cfm

_wave/robot/profile

_wave/robot/profile.cfm

_wave/robot/jsonrpc

_wave/robot/jsonrpc.cfm

 

  • Capabilites Hash - The Robot needs to provide a hash of its current capabilites each time it replies to the Wave Server. I think the idea here is that this should be changed when you decide to add or remove one of the events in capabilities.xml. It is 16 hex characters long so I used a built-in hash generator and trimmed it.

 

<cfset HashInput = "capabilitiesRevision000"><!--- Change this when you change your capabilities.xml --->

<cfset VersionHash = Right(Hash(string=HashInput, algorithm="CFMX_COMPAT"),16)> <!--- 16 char hash --->

 

  • Event Handling - there is a <cfswitch> statement that sends events to individual event handler functions. Simple for now.

 

  • Operations Queue - A simple queue (an Array) of Operation objects is built for the event handlers to generate operations. These are bundled up and serialized as JSON and sent back to the wave server.

 

What remains to Implement

  • The ColdFusion code I have so far is really just proof-of-concept. Once I have a working robot I plan to do the following :

 

Essential Reading

  • Official Wave API docs from Google
    • Google Wave API OverView Has a description of Wave Entities (and a diagram to help explain them). You need to know and undestand these key concepts :
      • Wave
      • Wavelet
      • Blips and Data Documents

clip_image003

  • The Events and Operations detail in the API explain the interactions between Robots and the Wave Server

 

  • Google App Engine on the OpenBD Wiki has some important information about what to expect when running ColdFusion on GAE. The essential things are :
    • No <CFFile> access or writing to the file system

 

  • Debugging Wave Robots : This essential article explains the technique of debugging your robot on a local development server and "faking" Wave JSON message bundles from your browser. It also explains the logs viewer on Google App Engine.

 

 

How To Build Your Own ColdFusion Wave Robots

 

  1. Setup Eclipse with the Google plug-in for Eclipse
    • There are good instructions for doing this :

 

  1. Download OpenBD for Google App Engine (make sure you get the Google Ap Engine bundle)

TIP: The configuration instructions are in the zip file that you dowload ( NOT in the Wiki)

 

  1. Create a GAE Project and add the JARs and configure OpenBD using the instruction above.

 

  1. Configure the project for a ColdFusion Wave Robot

 

  1. Copy the CFML source files to /war so that you have :

/war/_wave/robot/

  • capabilities.cfm
  • jsonrpc.cfm
  • profile.cfm

/war/_wave

  • index.cfm

/war/images

  • avatar.png

/war/static

  • profile.html
  • json2.js

 

  1. Edit /war/WEB-INF/web.xml and add the following :

 

<!-- Enable the URLRewriter. The Rules are configured seperately in urlrewrite.xml -->

<filter>

<filter-name>UrlRewriteFilter</filter-name>

<filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>UrlRewriteFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<!-- set the amount of seconds the conf file will be checked for reload

 can be a valid integer (0 denotes check every time,

 empty/not set denotes no reload check) -->

<init-param>

<param-name>confReloadCheckInterval</param-name>

<param-value>0</param-value>

</init-param>

 

And this is worth doing to :

 

 <!-- Comment out the welcome file list. Not essential, but less confusing with 404 errors

<welcome-file-list>

<welcome-file>index.cfm</welcome-file>

</welcome-file-list>

 -->

 

 

  1. Turn verbose logging on (.level = ALL) in /war/WEB-INF/logging.properties

 

HINT : If you don’t do this then you wont see any <cflog > output in your Google App Engine logs

 

# Set the default logging level for all loggers to WARNING

.level = ALL

 

Here is what your project layout should look like in Eclipse

 

clip_image004

 

  1. Run and Debug your Robot Locally

 

I have a slightly modified version of the browser-based JSON message POST tool in this article by Pamela Fox. The key to making this work for you is getting your hands on some real JSON message bundles from a wave session.

 

I created a second Robot sample in Java and implemented just the methods I wanted to get working in my ColdFusion version. I was able to copy the JSON message bundles from the Google App Engine Logs, and then use them locally.

 

<cflog text="#message#"> is your friend here. Log output displays in real-time locally in the Eclipse console, and quite quickly after a page refresh in the App Engine Logs (but make sure you have enabled verbose logging in the WEB-INF/logging.properties file)

 

Its ironic that JSON serialization turns out to be a great way to dump  messages sent and received to the console. You can dump the contents of a struct (say called properties) like this :

 

<!--- Dump the struct to the console  --->

<cflog text="HandleEvent: properties=#serializeJSON(properties)#">

 

  1. Deploy to Google App Engine and Debug

 

Once you have a local version running you will want to deploy to App Engine. This can take anywhere from 15 secs to 2 minutes so I don’t do it very often.

 

One difference between the App Engine and local environments is that the Wave Server encodes a binary version of the HTTP POST JSON content which I had to deal with like this :

 

<!--- Page Globals --->

<cfset HttpRequestData = GetHttpRequestData()>

 

<cffunction name="UnpackJSONRequest" output="No" returntype="Struct">

<!--- Get and deserialize the JSON Request --->

<!---  The JSON request is packed differenty depending how its called :

   * Google Wave Servers send the request as binary

   * Browser testing with XMLHTTPRequest sends request as text

--->   

<cfset pageContentStr = "">

<cfif IsBinary(HttpRequestData.CONTENT)>

<cfset pageContentStr = ToString(HttpRequestData.CONTENT)>

<cfelse>

<cfset pageContentStr = HttpRequestData.CONTENT>

</cfif>

<!--- LOG ---><cflog text="UnpackJSONRequest: #pageContentStr#">           

<cfset formStruct = deserializeJSON(pageContentStr)>

<cfreturn formStruct>

</cffunction>

 

 

TIP : Click the radio button Show: All requests to refresh the logs view.

clip_image005

 

 

  1. Add your Robot to a New Wave and watch the Google App Engine logs

 

Update 1 June : My robot is working and does the following.

  • Responds to a SELF_ADDED message with an introduction (which is a wavelet.appendBlip operation)
  • Responds to a BLIP_SUBMITTED event (that when you click Done or type Shift-Enter when typing ) event with a greeting (another wavelet.appendBlip operation)

 

image

 

 

 

CFML Source Code

 

Here is the ColdFusion source for the main files :

 

/war/_wave/robot/

  • capabilities.cfm
  • jsonrpc.cfm
  • profile.cfm

 

 

Capabilities.cfm

<cfsilent><cfset logpage = "_wave/robot/capabilities.cfm: ">

<cfcontent  type="text/xml">

<cfset hashInput = Now()>

<cfset versionHash = Right(Hash(string=hashInput, algorithm="CFMX_COMPAT"),16)>

<cflog text="#logpage# cgi.script_name= #cgi.script_name#">

</cfsilent><cfoutput>

<w:robot xmlns:w="http://wave.google.com/extensions/robots/1.0">

  <w:version>#versionHash#</w:version> 

  <w:protocolversion>0.21</w:protocolversion> 

  <w:capabilities> 

<!---      <w:capability name="DOCUMENT_CHANGED"/>  --->

      <w:capability name="WAVELET_SELF_ADDED"/>

<w:capability name="BLIP_SUBMITTED"/>

     <w:capability name="WAVELET_PARTICIPANTS_CHANGED"/>

  </w:capabilities> 

</w:robot>

</cfoutput>

 

Profile.cfm - updated 1 June

<!---  Profile.cfm

 

Requires :

cfjson CFC

http://www.epiphantastic.com/cfjson/  --->

<cfsilent><cfcontent type="application/json">

<cfset operationsObj = StructNew()>

<cfset operationsObj['name'] = "cfwaverobot">

<cfset operationsObj['imageUrl'] = "http://cfwavebot.appspot.com/images/avatar.png">

<cfset operationsObj['profileUrl'] = "http://cfwavebot.appspot.com/static/profile.html">

<cfinvoke component="json"

method="encode"

data= "#operationsObj#"

returnvariable="returnJSON" />

</cfsilent><cfoutput>#returnJSON#</cfoutput>

 

Jsonrpc.cfm - updated 1 June

<cftry><cfsilent>

<cfcontent  type="application/json">

<cfset logpage = "_wave/robot/jsonrpc.cfm: ">

<!---  JSONRPC.cfm

 

A Google Wave Robot written in ColdFusion

 

INPUT :

Message Bundle JSON (Expected as POST)

OUTPUT :

Operations JSON (MIME TYPE: application/json)

 

Requires :

cfjson CFC

http://www.epiphantastic.com/cfjson/

Find Out More :

http://blog.johnoscott.com/2010/05/building-wave-robots-in-coldfusion-with.html

 

--->

 

<!--- Page Globals --->

<cfset HttpRequestData = GetHttpRequestData()>

<!--- <cfset ResponseOperationsJSON = ""> ---> <!--- the JSON string we will be returning --->

<cfset OperationsArray = ArrayNew(1)>

<cfset OperationIntanceCount = 0><!--- Used to generate the id for each operation --->

<cfset RequestMessageBundle = null >

<cfset HashInput = "capabilitiesRevision000"><!--- Change this when you change your capabilities.xml --->

<cfset VersionHash = Right(Hash(string=HashInput, algorithm="CFMX_COMPAT"),16)> <!--- 16 char hash --->

<cfset RobotProtocolVersion = "0.21"> <!--- Not sure what this is but its used with each Operation object --->

 

<!--- LOGGING --->

<!--- <cflog text="#logpage# cgi.script_name= #cgi.script_name#">

<cflog text="#logpage# cgi.REQUEST_METHOD= #cgi.REQUEST_METHOD#">

<cflog text="#logpage# cgi.CONTENT_TYPE= #cgi.CONTENT_TYPE#"> --->

<!--- LOG: FORM --->

<!--- <cflog text="StructCount(Form)=#StructCount(Form)#">

<cfset fieldNum = 1>

<cfloop collection="#Form#" ITEM="VarName">

<cflog text="#logpage# Form Field #fieldNum# Name=#VarName#; Value=#Form[VarName]#">

<cfset fieldNum = fieldNum + 1>

</cfloop> --->

<!--- LOG: HTTPREQUESTDATA --->

<!--- <cflog text="StructCount(HttpRequestData)=#StructCount(HttpRequestData)#">

<cflog text="All HttpRequestData=#CustomSerializeJSON(HttpRequestData)#">

<cflog text="StructCount(HttpRequestData.HEADERS)=#StructCount(HttpRequestData.HEADERS)#">

<cflog text="HttpRequestData.HEADERS=#CustomSerializeJSON(HttpRequestData.HEADERS)#">

<cfset fieldNum = 1>

<cfloop collection="#HttpRequestData.HEADERS#" ITEM="key">

<cflog text="#logpage# Header:#fieldNum# #key#:#HttpRequestData.HEADERS[key]#">

<cfset fieldNum = fieldNum + 1>

</cfloop> --->

<!--- CONTENT --->

<!--- <cfif IsBinary(HttpRequestData.CONTENT)>

<cflog text="Type=BINARY for HttpRequestData.CONTENT of Length #Len(HttpRequestData.CONTENT)#">

<cflog text="Attempting to Unpack BINARY CONTENT">

<cfset content = ToString(#HttpRequestData.CONTENT#)>

<cflog text="Unpacked BINARY HttpRequestData.CONTENT= (#content#)">

<cfelse>

<cflog text="Type=String for HttpRequestData.CONTENT=#HttpRequestData.CONTENT#">

</cfif>

<cflog text="HttpRequestData.METHOD=#HttpRequestData.METHOD#">

<cflog text="HttpRequestData.PROTOCOL=#HttpRequestData.PROTOCOL#">

 

<cflog text="CGI=#CustomSerializeJSON(CGI)#">

<cflog text="URL=#CustomSerializeJSON(URL)#"> --->

 

 

 

<!--- FUNCTIONS --->

 

<cffunction name="GetStructDump" output="No" returntype="String">

<cfargument name="theStruct" type="Struct" required="Yes">

<cfargument name="theDepth" type="Numeric" required="No">

<!---

<cfset i = 1>

<cfset dumpStr = "">

<cfloop collection="#Form#" ITEM="VarName">

<cflog text="#logpage# Form Field #fieldNum# Name=#VarName#; Value=#Form[VarName]#">

<cfset fieldNum = fieldNum + 1>

</cfloop>

 --->

<cfreturn CustomSerializeJSON(theStruct)>

</cffunction>

 

 

<cffunction name="UnpackJSONRequest" output="No" returntype="Struct">

<!--- Get and deserialize the JSON Request --->

<!---  The JSON request is packed differenty depending how its called :

   * Google Wave Servers send the request as binary

   * Browser testing with XMLHTTPRequest sends request as text

--->   

<cfset pageContentStr = "">

<cfif IsBinary(HttpRequestData.CONTENT)>

<cfset pageContentStr = ToString(HttpRequestData.CONTENT)>

<cfelse>

<cfset pageContentStr = HttpRequestData.CONTENT>

</cfif>

<!--- LOG ---><cflog text="UnpackJSONRequest: #pageContentStr#">           

<cfset formStruct = deserializeJSON(pageContentStr)>

<cfreturn formStruct>

</cffunction>

 

 

<!--- EVENT HANDLERS --->

 

<cffunction name="ProcessEvents" output="No" >

<!--- HANDLE THE EVENTS a--->

<cfset events = RequestMessageBundle["EVENTS"]>

<cfif IsArray(events)>

<!--- <cflog text="#logpage# WE GOT EVENTS:" > --->

<cfloop Index="i" From="1" To="#ArrayLen(events)#">

<!--- LOG ---><cflog text="Event[#i#]  = #CustomSerializeJSON(events[i])#">           

<cftry>

<!--- HANDLE THE EVENT AND GET A RESPONSE OPERATION --->

<!--- The event will queue an operation if it wants to which will be returned with the page response --->

<cfset dontCare = HandleEvent(events[i])>

<!--- <cflog text="Event[#i#] Result RequestMessageBundle = #resultOperationsJSON#"> --->       

<cfcatch type="any">

<!--- LOG ---><cflog text="** ProcessEvents: Event:#i# cfcatch caught error: #cfcatch.Message# *********">               

</cfcatch>

</cftry>

</cfloop>

<cfelse>

<!--- LOG ---><cflog text="#logpage# NO EVENTS" >

</cfif>

<cfreturn>

</cffunction>

 

 

<cffunction name="HandleEvent" output="No">

<cfargument name="event" type="Struct" required="Yes">

<!--- Unpack the event and dispatch to its event handler --->

<!--- An Event looks something like this :

{     "MODIFIEDBY":"johnoscott@googlewave.com",

      "PROPERTIES": {"BLIPID":"b+N-PjdA1UO","PARTICIPANTSREMOVED":[],"PARTICIPANTSADDED":["cfwavebot@appspot.com"]},

"TIMESTAMP":1.27519999508E+12,

"TYPE":"WAVELET_PARTICIPANTS_CHANGED"

}

--->

<cfset properties = event["PROPERTIES"]>

<!--- <cflog text="HandleEvent: properties = #CustomSerializeJSON(properties)#"> --->

<cfset eventType = event["TYPE"]>

<!--- <cflog text="HandleEvent:eventType = #eventType#"> --->

<cfset result="">

<cfswitch expression = "#eventtype#">

<cfcase value = "BLIP_SUBMITTED">

<cfset result = BlipSubmittedEvent(event)>

</cfcase>

<cfcase value = "WAVELET_SELF_ADDED">

<cfset result = WaveletSelfAddedEvent(event)>

</cfcase>

<cfcase value = "WAVELET_PARTICIPANTS_CHANGED">

<cfset result = WaveletParticipantsChangedEvent(event)>

</cfcase>

</cfswitch>

<cfreturn>

 

</cffunction>

 

<cffunction name="BlipSubmittedEvent" output="No" >

<cfargument name="event" type="Struct" required="Yes">

<!--- Sample Event

{

    "type": "BLIP_SUBMITTED",

"modifiedBy": "user@example.com",

    "timestamp": 1255935016481,

    "properties": {

          "blipId": "b+ja8F_Hw4J"

}

    }

 --->

<!--- LOG ---><cflog text="BlipSubmittedEvent:" >

<cfset properties = event["properties"]>

<cfset blipID = properties["blipId"]>

<!--- LOG ---><cflog text="BlipSubmittedEvent: blipID=#blipID#" >

<!--- <cflog text="HandleEvent: properties = #CustomSerializeJSON(properties)#"> --->

<cfset content = getBlipContent(blipID)>

<!--- LOG ---> <cflog text="BlipSubmittedEvent: content = #content#">

 

<!--- Look for signs of CF-speaking bretheran :) --->

<cfif content EQ "">

<cfreturn>

<cfelseif findNoCase('hello',content)>

<cfset replyStr = "And a <cfgood_day_to_you_too> !">

</cfif>

<cfset dontCare = WaveletReply(replyStr, event)>

<cfreturn>

</cffunction>

 

<cffunction name="WaveletSelfAddedEvent" output="No" >

<cfargument name="event" type="Struct" required="Yes">

<!--- Event looks something like this :

{     "MODIFIEDBY":"johnoscott@googlewave.com",

      "PROPERTIES": {"BLIPID":"b+N-PjdA1UO","PARTICIPANTSREMOVED":[],"PARTICIPANTSADDED":["cfwavebot@appspot.com"]},

"TIMESTAMP":1.27519999508E+12,

"TYPE":"WAVELET_PARTICIPANTS_CHANGED"

}

--->

<!--- LOG ---><cflog text="WaveletSelfAddedEvent:" >

<!--- <cfset properties = event["PROPERTIES"]> --->

<!--- <cflog text="HandleEvent: properties = #CustomSerializeJSON(properties)#"> --->

<cfset replyStr = "Howdy, I'm a ColdFusion robot.\n" &

"I only do one thing right now, and that is to reply to " &

"a BLIP_SUBMITTED event if it contains a greeting (hint : starts with hel..). " &

"Find out how i came into being here : http://bit.ly/dhQRQn"   

>

<cfset dontCare = WaveletReply(replyStr, event)>

 

<cfreturn>

</cffunction>

 

<cffunction name="WaveletParticipantsChangedEvent" output="No">

<cfargument name="event" type="Struct" required="Yes">

<!--- Event looks something like this :

{     "MODIFIEDBY":"johnoscott@googlewave.com",

      "PROPERTIES": {"BLIPID":"b+N-PjdA1UO","PARTICIPANTSREMOVED":[],"PARTICIPANTSADDED":["cfwavebot@appspot.com"]},

"TIMESTAMP":1.27519999508E+12,

"TYPE":"WAVELET_PARTICIPANTS_CHANGED"

}

--->

<!--- LOG ---><cflog text="WaveletParticipantsChangedEvent:" >

<cfset properties = event["PROPERTIES"]>

<!--- <cflog text="HandleEvent: properties = #CustomSerializeJSON(properties)#"> --->

<cfreturn>

</cffunction>

 

 

<!--- OPERATIONS --->

 

<!--- Return a new Operation structure --->

<!--- RequestMessageBundle to return look like this, an array of operations

[    {    "method":"robot.notifyCapabilitiesHash",

"id":"op1",

"params":{"protocolVersion":"0.21","capabilitiesHash":"ffffff79d8b7888c"}

},

{    "method":"wavelet.appendBlip",

"id":"op2",

"params":{    "waveId":"googlewave.com!w+jH65e-xWW",

"waveletId":"googlewave.com!conv+root",

"blipData":{"annotations":[],

"elements":{},

"blipId":"TBD_googlewave.com!conv+root_1771192937",

"childBlipIds":[],

"contributors":[],

"creator":null,

"content":"\nHi everybody! MY JOB IS TO STOP EVERYONE FROM SHOUTING",

"lastModifiedTime":-1,

"parentBlipId":null,

"version":-1,

"waveId":"googlewave.com!w+jH65e-xWW",

"waveletId":"googlewave.com!conv+root"}

}

}

]

 

--->

<cffunction name="InitOperationsQueue" output="No">

<!--- Initialize the Queue and add queue the first operation, the capabilities operation --->

<!--- OperationsArray is initialized at the top of the page --->

<!--- <cfset OperationsArray = ArrayNew(1)> --->

<!--- Get the Capabilites Operation and queue it --->

<cfset capabilitiesOperation = GetCapabilitiesOperation()>

<cfset dontCare = QueueOperation(capabilitiesOperation) >

<cfreturn>

</cffunction>

<cffunction name="QueueOperation" output="No">

<cfargument name="operation" type="Struct" required="Yes">

<!--- Queue the operation to be returned when the page returns --->

<!--- LOG ---><cflog text="QueueOperation: id=#operation.ID# json=#CustomSerializeJSON(operation)#">

<cfset ArrayAppend(OperationsArray,operation) >

<cfreturn>

</cffunction>

 

 

<cffunction name="NewOperation" output="No" returntype="Struct">

<!--- RETURNS a new operations struct --->

<!--- Example :

{

"method":"undefined",

"id":"op1",

"params":{}

}

 --->

<cfset OperationIntanceCount = OperationIntanceCount + 1 >

<cfset o = StructNew()>

<cfset o['method'] = "undefined">

<cfset o['id'] = "op#OperationIntanceCount#">

<cfset o['params'] = StructNew()>

<cfreturn o>

</cffunction>

 

 

<cffunction name="GetCapabilitiesOperation" output="No">

<!--- RETURNS a Capabilities Operation --->

<!--- There is always one of these send back with our operations response --->

<cfset o = NewOperation()>

<!--- Example :

"method":"undefined",

"id":"op1",

"params":{"protocolVersion":"0.21","capabilitiesHash":"ffffff79d8b7888c"}

},

 --->

<cfset o['method'] = "robot.notifyCapabilitiesHash">

     <cfset o['params']['protocolVersion'] = "#RobotProtocolVersion#">

<cfset o['params']['capabilitiesHash'] = "#VersionHash#">

<cfreturn o>

</cffunction>

 

<cffunction name="GetQueuedOperationsJSON" output="No" returntype="String">

<!--- Queue the operation to be returned when the page returns --->

<!--- GENERATE THE RESPONSE --->

 

<!--- LOG ---><cflog text="GetQueuedOperationsJSON: #ArrayLen(OperationsArray)# Operation(s) to return :">

<cfloop Index="i" From="1" To="#ArrayLen(OperationsArray)#">

<cfset o = OperationsArray[i]>

<!--- LOG ---><cflog text="-- Operation:  #i# #o.Method# as json:#CustomSerializeJSON(o)#">

</cfloop>

 

<cfset operationsArrayJSON = CustomSerializeJSON(OperationsArray)>

 

<cfreturn operationsArrayJSON>

</cffunction>

 

 

<cffunction name="GetBlipContent" output="No" returntype="String">

<cfargument name="blipID" type="String" required="Yes">

<!--- Return Blip.Content for the first blip --->

<cfscript>

try {

return RequestMessageBundle.blips[blipId].content;

} catch (Any e) {

return "";       

}

</cfscript>

</cffunction>

 

 

<cffunction name="GetWavelet" output="No" returntype="Struct">

<!--- RETURNS a new wavelet struct --->

<!--- Example :

{

"waveId":"googlewave.com!w+jH65e-xWW",

"waveletId":"googlewave.com!conv+root",

}

 --->

<!---

Get the Wavelet object from the Message Bundle

It looks something like this

"wavelet": {

"creationTime": 1255934856713,

    "creator": "user@example.com",

    "lastModifiedTime": 1255935016481,

    "participants": [ "user@example.com","user2@example.com" ],

    "rootBlipId": "b+ja8F_Hw4J",

    "title": "",

    "version": 11,

    "waveId": "example.com!w+ja8F_Hw4I",

    "waveletId": "example.com!conv+root",

    "dataDocuments": { }

}

     --->

<cfset w = StructNew()>

<cfset messageWavelet = RequestMessageBundle['wavelet']>

<cfif IsStruct(messageWavelet) >

// Create the return structure

<cfset w['waveId'] = messageWavelet.waveId>

<cfset w['waveletId'] = messageWavelet.waveletId>       

<cfelse>

<!--- LOG ---><cflog text="GetWavelet: *** ERROR *** Cant get Wavelet">

</cfif>

<cfreturn w>

</cffunction>

 

 

 

<cffunction name="NewBlip" output="No" returntype="Struct">

<!--- RETURNS a new Blip struct --->

<cfargument name="blipID" type="String" required="Yes">

<cfargument name="wavelet" type="Struct" required="Yes">

<cfargument name="reply" type="String" required="Yes">

<!--- Example :

{               

"blipId":"TBD_googlewave.com!conv+root_-1113654217",

"annotations":[],

"elements":{},

"childBlipIds":[],

"contributors":[],

"content":"\nHi everybody, I just got added to the wave.",

"lastModifiedTime":-1,

"version":-1,

"waveId":"googlewave.com!w+N-PjdA1UX",

"waveletId":"googlewave.com!conv+root"

}

 --->

 <cfscript>

 blip = StructNew();

 blip['blipId'] = "#blipID#"; // Not sure if this should be the same

 blip['annotations'] = ArrayNew(1);

 blip['elements'] = StructNew();

 blip['childBlipIds']= ArrayNew(1);

 blip['contributors']= ArrayNew(1);

 blip['content'] = Chr(10) & "#reply#"; // Must start with a newline

 blip['lastModifiedTime'] = -1;

 blip['version'] = -1;

 blip['waveId'] = wavelet.waveId;

 blip['waveletId']= wavelet.waveletId;

 return blip;

 </cfscript>

</cffunction>

 

 

<cffunction name="WaveletReply" output="No">

<cfargument name="reply" type="String" required="Yes">

<cfargument name="event" type="Struct" required="Yes">

<!--- Append a reply to the current wavelet --->

<!---     Example Operation looks like this

{    "method":"wavelet.appendBlip",

"id":"op2",

"params":{    "waveId":"googlewave.com!w+jH65e-xWW",

"waveletId":"googlewave.com!conv+root",

"blipData":{"annotations":[],

"elements":{},

"blipId":"TBD_googlewave.com!conv+root_1771192937",

"childBlipIds":[],

"contributors":[],

"creator":null,

"content":"\nHi everybody! MY JOB IS TO STOP EVERYONE FROM SHOUTING",

"lastModifiedTime":-1,

"parentBlipId":null,

"version":-1,

"waveId":"googlewave.com!w+jH65e-xWW",

"waveletId":"googlewave.com!conv+root"}

}

} --->

<cfscript>

// Start with an empty operation

o = NewOperation();

// Set the method

o['method'] = "wavelet.appendBlip";

// Set the Wave and Wavelet

w = GetWavelet();

o['params']['waveId'] = w.waveID;

o['params']['waveletId'] = w.waveletId;

// Add the Blip with our reply in it

// blipID = event.properties.blipID; // get the blipID from the event

blipID = "TBD_#w.waveletId#_-239145996"; // TBD_googlewave.com!conv+root_-239145996

o['params']['blipData'] = NewBlip(blipID, w, reply);   

// Add it to the queue

QueueOperation(o);

</cfscript>

<cfreturn  >

</cffunction>

 

<!--- Utilities --->

<cffunction name="CustomSerializeJSON" output="No" returntype="String">

<cfargument name="o" required="Yes">

<!--- OpenBD can lowercase JSON serializations http://wiki.openbluedragon.org/wiki/index.php/SerializeJSON() --->

<!--- <cfreturn serializeJSON(o, false, true)> --->

<!--- I ended up using this JSON serializer that honours the case of the keys (e.g. waveletId) --->

<!--- http://www.epiphantastic.com/cfjson/ --->

<cfinvoke component="json"

method="encode"

data= "#o#"

returnvariable="returnJSON" />

<cfreturn returnJSON>

</cffunction>

 

 

<!---  --------------------------------------------------------------------- --->

 

<cfscript>

/*

 * MAIN PAGE EXECUTION

 */

 

// --- UNPACK THE MESSAGE BUNDLE ---

RequestMessageBundle = UnpackJSONRequest(); // sets the global RequestMessageBundle

// --- Initialise the Operations Queue ---

InitOperationsQueue();

// --- Handle the Events ---

ProcessEvents();

</cfscript>

 

 

<!--- Get the return Operations --->

<!--- LOG ---><cflog text="GetQueuedOperationsJSON: ">

<cfset ResponseOperationsJSON = GetQueuedOperationsJSON() >

 

<!--- Return Page Output --->

 

<!--- LOG ---><cflog text=">>>> We are done ! Returning Operations as JSON: #ResponseOperationsJSON# ">

</cfsilent><cfoutput>#ResponseOperationsJSON#</cfoutput>

<cfcatch type="any"><cfoutput>[{"error":"Please POST me a JSON Message Bundle"}]</cfoutput>

<!--- LOG ---><cflog text="******************** cfcatch caught error: #cfcatch.Message# ********************************">

</cfcatch>

</cftry>

2 comments:

sharper said...

Interesting stuff. Thanks for posting this, Johno.

Laser Pegs said...

hey its reall such a nice