"events": [
{
"type": "WAVELET_SELF_ADDED",
"modifiedBy": "pamela.fox@wavesandbox.com",
"timestamp": 1269400482868,
"properties": {
"blipId": "b+FYYeTpCXJ"
}
}
],
"wavelet": {
"creationTime": 1269400451853,
"lastModifiedTime": 1269400482868,
"version": 14,
"participants": [
"pamela.fox@wavesandbox.com",
"emo-bot@appspot.com"
],
"creator": "pamela.fox@wavesandbox.com",
"rootBlipId": "b+FYYeTpCXJ",
"title": "Dear Diary,",
"waveId": "wavesandbox.com!w+FYYeTpCXI",
"waveletId": "wavesandbox.com!conv+root"
},
{"params": {
"blipId": "b+FYYeTpCXJ",
"waveletId": "wavesandbox.com!conv+root",
"waveId": "wavesandbox.com!w+FYYeTpCXI",
"modifyAction": {
"modifyHow": "REPLACE",
"elements": [
{
"type": "IMAGE",
"properties": {
"url": "http://emo-bot.appspot.com/smile.gif"
}
}
]
},
"modifyQuery": {
"textMatch": ":)",
"maxRes": -1
}
},
"method": "document.modify",
"id": "op2"
}
Events
Operations
Robot
Wave
Wave
Building the Wave Robots API
import logging
from waveapi import appengine_robot_runner
from waveapi import element
from waveapi import events
from waveapi import ops
from waveapi import robot
anim_base = 'http://emo-bot.appspot.com/'
anim_ext = '.gif'
emoticons = {
':)' : 'smile',
':(' : 'frown',
'(heart)': 'heart'
}
def ProcessBlip(event, wavelet):
blip = event.blip
for emoticon in emoticons:
r = blip.all(emoticon)
if r:
r.replace(element.Image(anim_base + emoticons[emoticon] + anim_ext))
pass
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
appengine_robot_runner.run(emobot, debug=True)
Profiles
http://emo-bot.appspot.com/_wave/robot/profile
{"profileUrl": "http://emo-bot.appspot.com",
"imageUrl": "https://emo-bot.appspot.com/public/thumbnail.png",
"name": "Emo Bot"}
app.yaml
application: emo-bot
version: 1
runtime: python
handlers:
- url: /_wave/.*
script: main.py
- url: /public/(.*)
static_files: public/\1
upload: public/(.*)
import logging
from waveapi import appengine_robot_runner
from waveapi import element
from waveapi import events
from waveapi import ops
from waveapi import robot
anim_base = 'http://wave-skimmy.appspot.com'
anim_ext = '.gif'
emoticons = {
':)' : 'smile',
':(' : 'frown',
'(heart)': 'heart'
}
def ProcessBlip(event, wavelet):
blip = event.blip
for emoticon in emoticons:
r = blip.all(emoticon)
if r:
r.replace(element.Image(anim_base + emoticons[emoticon] + anim_ext))
pass
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
appengine_robot_runner.run(emobot, debug=True)
main.py
....
class ProfileHandler(webapp.RequestHandler):
def __init__(self, method, contenttype):
self._method = method
self._contenttype = contenttype
def get(self):
self.response.headers['Content-Type'] = self._contenttype
self.response.out.write(self._method())
def create_robot_webapp(robot):
return webapp.WSGIApplication([
('.*/_wave/robot/profile',
lambda: ProfileHandler(
robot.profile_json,
'application/json')),
('.*/_wave/capabilities.xml',
lambda: CapabilitiesHandler(
robot.capabilities_xml,
'application/xml')),
('.*/_wave/robot/jsonrpc',
lambda: RobotEventHandler(robot)),
('.*/_wave/verify_token',
lambda: RobotVerifyTokenHandler(robot)),
])
def run(robot):
app = create_robot_webapp(robot)
run_wsgi_app(app)
appengine_robot_runner.py
robot.py
...
def profile_json(self):
data = {'name': self.name,
'imageUrl': self.image_url,
'profileUrl': self.profile_url}
return simplejson.dumps(data)
...
Capabilities
which protocol version?
which events?
Incoming JSON
Outgoing JSON
http://emo-bot.appspot.com/_wave/capabilities.xml
<w:robot xmlns:w="http://wave.google.com/extensions/robots/1.0">
<w:version>0xc5d908e</w:version>
<w:protocolversion>0.2</w:protocolversion>
<w:capabilities>
<w:capability name="BLIP_SUBMITTED"/>
<w:capability name="DOCUMENT_CHANGED"/>
<w:capability name="OPERATION_ERROR"/>
</w:capabilities>
</w:robot>
application: emo-bot
version: 1
runtime: python
handlers:
- url: /_wave/.*
script: main.py
- url: /public/(.*)
static_files: public/\1
upload: public/(.*)
import logging
from waveapi import appengine_robot_runner
from waveapi import element
from waveapi import events
from waveapi import ops
from waveapi import robot
anim_base = 'http://emo-bot.appspot.com'
anim_ext = '.gif'
emoticons = {
':)' : 'smile',
':(' : 'frown',
'(heart)': 'heart'
}
def ProcessBlip(event, wavelet):
blip = event.blip
for emoticon in emoticons:
r = blip.all(emoticon)
if r:
r.replace(element.Image(anim_base + emoticons[emoticon] + anim_ext))
pass
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
appengine_robot_runner.run(emobot, debug=True)
appengine_robot_runner.py
robot.py
class CapabilitiesHandler(webapp.RequestHandler):
def __init__(self, method, contenttype):
self._method = method
self._contenttype = contenttype
def get(self):
self.response.headers['Content-Type'] = self._contenttype
self.response.out.write(self._method())
def create_robot_webapp(robot):
return webapp.WSGIApplication([
('.*/_wave/robot/profile',
lambda: ProfileHandler(
robot.profile_json,
'application/json')),
('.*/_wave/capabilities.xml',
lambda: CapabilitiesHandler(
robot.capabilities_xml,
'application/xml')),
('.*/_wave/robot/jsonrpc',
lambda: RobotEventHandler(robot)),
('.*/_wave/verify_token',
lambda: RobotVerifyTokenHandler(robot)),
])
def run(robot):
app = create_robot_webapp(robot)
run_wsgi_app(app)
app.yaml
main.py
def capabilities_xml(self):
"""Return this robot's capabilities as an XML string."""
lines = []
for capability, payloads in self._handlers.items():
for payload in payloads:
handler, event_class, context, filter = payload
line = ' <w:capability name="%s" >/\n' % capability
lines.append(line)
return ('<?xml version="1.0"?>\n'
'<w:robot xmlns:w="http://.../robots/1.0">\n'
'<w:version>%s</w:version>\n'
'<w:protocolversion>%s</w:protocolversion>\n'
'<w:capabilities>\n'
'%s'
'</w:capabilities>\n'
'</w:robot>\n') % (self.capabilities_hash(),
ops.PROTOCOL_VERSION,
'\n'.join(lines))
[
{
"method": "robot.notifyCapabilitiesHash",
"params": {
"capabilitiesHash": "0xc5d908e"
}
}
]
cache:
emo-bot.appspot.com
0x10962d0
Outgoing JSON
http://emo-bot.appspot.com/_wave/capabilities.xml
<w:robot xmlns:w="http://wave.google.com/extensions/robots/1.0">
<w:version>0xc5d908e</w:version>
<w:protocolversion>0.2</w:protocolversion>
<w:capabilities>
<w:capability name="BLIP_SUBMITTED"/>
<w:capability name="DOCUMENT_CHANGED"/>
<w:capability name="OPERATION_ERROR"/>
</w:capabilities>
</w:robot>
Problem
Solution
Auto Generation
<w:robot xmlns:w="http://wave.google.com/extensions/robots/1.0">
<w:version>0xc5d908e</w:version>
<w:protocolversion>0.2</w:protocolversion>
<w:capabilities>
<w:capability name="BLIP_SUBMITTED"/>
<w:capability name="DOCUMENT_CHANGED"/>
<w:capability name="OPERATION_ERROR"/>
</w:capabilities>
</w:robot>
http://emo-bot.appspot.com/_wave/capabilities.xml
Problem
Solution
def register_handler(self, event_class, handler, context=None, filter=None):
payload = (handler, event_class, context, filter)
self._handlers.append(payload)
self._capability_hash = (self._capability_hash * 13 +
hash(ops.PROTOCOL_VERSION) +
hash(event_class.type) +
hash(context) +
hash(filter)) & 0xfffffff
def process_events(self, json):
parsed = simplejson.loads(json)
pending_ops = ops.OperationQueue()
event_wavelet = self._wavelet_from_json(parsed, pending_ops)
for event_data in parsed['events']:
for payload in self._handlers.get(event_data['type'], []):
handler, event_class, context, filter = payload
event = event_class(event_data, event_wavelet)
handler(event, event_wavelet)
pending_ops.set_capability_hash(self.capabilities_hash())
return simplejson.dumps(pending_ops.serialize())
appengine_robot_runner.py
main.py
application: emo-bot
version: 1
runtime: python
handlers:
- url: /_wave/.*
script: main.py
- url: /public/(.*)
static_files: public/\1
upload: public/(.*)
app.yaml
import logging
from waveapi import appengine_robot_runner
from waveapi import element
from waveapi import events
from waveapi import ops
from waveapi import robot
anim_base = 'http://emo-bot.appspot.com'
anim_ext = '.gif'
emoticons = {
':)' : 'smile',
':(' : 'frown',
'(heart)': 'heart'
}
def ProcessBlip(event, wavelet):
blip = event.blip
for emoticon in emoticons:
r = blip.all(emoticon)
if r:
r.replace(element.Image(anim_base + emoticons[emoticon] + anim_ext))
pass
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
appengine_robot_runner.run(emobot, debug=True)
class RobotEventHandler(webapp.RequestHandler):
def __init__(self, robot):
self._robot = robot
def post(self):
json_body = self.request.body
json_response = self._robot.process_events(json_body)
self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
self.response.out.write(json_response.encode('utf-8'))
def create_robot_webapp(robot):
return webapp.WSGIApplication([
('.*/_wave/robot/profile',
lambda: ProfileHandler(
robot.profile_json,
'application/json')),
('.*/_wave/capabilities.xml',
lambda: CapabilitiesHandler(
robot.capabilities_xml,
'application/xml')),
('.*/_wave/robot/jsonrpc',
lambda: RobotEventHandler(robot)),
('.*/_wave/verify_token',
lambda: RobotVerifyTokenHandler(robot)),
])
def run(robot):
app = create_robot_webapp(robot)
run_wsgi_app(app)
Auto Generation
robot.py
Capabilities Hash
{"params": {
"blipId": "b+FYYeTpCXJ",
"waveletId": "wavesandbox.com!conv+root",
"waveId": "wavesandbox.com!w+FYYeTpCXI",
"modifyAction": {
"modifyHow": "REPLACE",
"elements": [
{
"type": "IMAGE",
"properties": {
"url": "http://wave-skimmy.appspot.com/smile.gif"
}
}
]
},
"modifyQuery": {
"textMatch": ":)",
"maxRes": -1
}
},
"method": "document.modify",
"id": "op2"
}
Outgoing JSON
Problem
Solution
Outgoing JSON
{"params": {
"blipId": "b+FYYeTpCXJ",
"waveletId": "wavesandbox.com!conv+root",
"waveId": "wavesandbox.com!w+FYYeTpCXI",
"modifyAction": {
"modifyHow": "REPLACE",
"elements": [
{
"type": "IMAGE",
"properties": {
"url": "http://wave-skimmy.appspot.com/smile.gif"
}
}
]
},
"modifyQuery": {
"textMatch": ":)",
"maxRes": -1
}
},
"method": "document.modify",
"id": "op2"
}
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
emobot.set_verification_token_info('AOijR2fFR5awOGHmU...', '4203')
appengine_robot_runner.run(emobot, debug=True)
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
emobot.setup_oauth('TOPSECRETKEY', 'TOPSECRETSECRET')
appengine_robot_runner.run(emobot, debug=True)
def make_rpc(self, operations):
data = simplejson.dumps(queue.serialize())
oauth_request = oauth.OAuthRequest.from_consumer_and_token(
self._consumer, http_method='POST', http_url=self._server_rpc_base)
oauth_request.sign_request(WaveService.SIGNATURE_METHOD, self._consumer)
headers = {'Content-Type': 'application/json'}
headers.update(oauth_request.to_header());
status, content = self._http_post(
url=self._server_rpc_base, data=data, headers=headers)
class RobotVerifyTokenHandler(webapp.RequestHandler):
def __init__(self, robot):
self._robot = robot
def get(self):
token, st = self._robot.get_verification_token_info()
if self.request.get('st') != st:
self.response.out.write('Invalid st value passed')
return
self.response.out.write(token)
def create_robot_webapp(robot):
return webapp.WSGIApplication([('.*/_wave/capabilities.xml',
lambda: CapabilitiesHandler(
robot.capabilities_xml,
'application/xml')),
('.*/_wave/robot/profile',
lambda: ProfileHandler(
robot.profile_json,
'application/json')),
('.*/_wave/robot/jsonrpc',
lambda: RobotEventHandler(robot)),
('.*/_wave/verify_token',
lambda: RobotVerifyTokenHandler(robot)),
])
appengine_robot_runner.py
main.py
waveservice.py
main.py
OAuth Handling
Active Operations
Problem
webapp
+
+
+
__init__.py
appengine_robot_runner.py
blip.py
element.py
errors.py
events.py
oauth/
ops.py
robot.py
search.py
simplejson/
util.py
wavelet.py
waveservice.py
webapp
+
class CapabilitiesHandler(webapp.RequestHandler)
class RobotEventHandler(webapp.RequestHandler)
class ProfileHandler(webapp.RequestHandler)
class RobotVerifyTokenHandler(webapp.RequestHandler)
def appengine_post(url, data, headers)
def operation_error_handler(event, wavelet)
def create_robot_webapp(robot, debug=False)
def run(robot, debug=False, log_errors=True)
Solution
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^_wave/*', 'testproject.testbot.wave.OnWaveAnything')
)
import logging
from waveapi import django_robot_runner
from waveapi import element
from waveapi import events
from waveapi import ops
from waveapi import robot
anim_base = 'http://emo-bot.appspot.com'
anim_ext = '.gif'
emoticons = {
':)' : 'smile',
':(' : 'frown',
'(heart)': 'heart'
}
def ProcessBlip(event, wavelet):
blip = event.blip
for emoticon in emoticons:
r = blip.all(emoticon)
if r:
r.replace(element.Image(anim_base + emoticons[emoticon] + anim_ext))
pass
def OnWaveAnything(request):
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
emobot.setup_oauth(credentials.KEY, credentials.SECRET)
return django_robot_runner.handle(request, emobot)
import logging
import sys
from django.http import HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import render_to_response
import events
def CapabilitiesHandler(request, robot):
return HttpResponse(robot.capabilities_xml(), content_type='text/xml')
def ProfileHandler(request, robot):
response = robot.profile_json()
return HttpResponse(response, content_type='text/plain')
def RobotEventHandler(request, robot):
if request.method == 'POST':
json_body = request.raw_post_data
json_body = unicode(json_body, 'utf8')
json_response = robot.process_events(json_body)
response = (json_response.encode('utf-8'))
return HttpResponse(response, content_type='application/json; charset=utf-8')
else:
return HttpResponse('GET not implemented')
def operation_error_handler(event, wavelet):
if isinstance(event, events.OperationError):
logging.error('Previously operation failed: id=%s, message: %s',
event.operation_id, event.error_message)
def RobotVerifyTokenHandler(request, robot):
token, st = robot.get_verification_token_info()
if request.GET['st'] != st:
response = 'Invalid st value passed'
return
response = token
return HttpResponse(response)
def handle(request, robot):
robot.register_handler(events.OperationError, operation_error_handler)
if request.path.find('_wave/capabilities.xml') > -1:
return CapabilitiesHandler(request, robot)
if request.path.find('_wave/robot/profile') > -1:
return ProfileHandler(request, robot)
if request.path.find('_wave/robot/jsonrpc') > -1:
return RobotEventHandler(request, robot)
if request.path.find('_wave/verify_token') > -1:
return RobotVerifyTokenHandler(request, robot)
+
urls.py
wave.py
django_robot_runner.py
Non-App Engine Robots
Behind the Scenes
Google Maps APIs
Google Wave APIs
Spreadsheets, Gadgets, App Engine, Blogger, Calendar, Picasa,
OpenSocial, Flickr, Amazon, ...
JavaScript
Python
, PHP
, JavaScript
, Java
About Me
Wave Robots API v1
Wave Developer Preview
June 1, 2009
March 1, 2010
Wave Robots API v2
Wave Consumer Preview
Oct. 1, 2009
API History
wave
wavelet
wavelet
conv+O4Fsp5kfB
conv+root
wavesandbox.com!w+YTiBcVOfA
wavelet
conv+root
blip
b+O4Fsp5kfA
b+YTiBcVOfB
blip
blip
b+YTiBcVOfD
blip
b+Dad5DLZkB
tags
participants
<body>
<line a="c"></line>
The pizza was awesome,
can't believe you missed it!
<line></line>
<image url="http://www.p.com/p.gif">
<line></line>More pics here.
</body>
(17,24) : style/fontSize=1.3em
(17,24) : style/fontWeight=bold
(72,76) : link=http://www.p.com/p.gif
(1,77) : lang=en
b+Dad5DLZkB
blip
Google Wave Conversation Model
Operational Transforms
document
items+positions
operations
skip, insert, delete
insert 'ABCDE'
skip 3, delete 1
Lessons Learned
Automation
Versioning
Authentication
Cross-Framework
Language-Appropriate
Google Wave Robot Python API v2
============================================
The :mod:`blip` Module
-------------------------
.. automodule:: blip
:members:
:undoc-members:
The :mod:`wavelet` Module
-------------------------
.. automodule:: wavelet
:members:
:undoc-members:
The :mod:`robot` Module
-------------------------
.. automodule:: robot
:members:
:undoc-members:
index.rst
...
class Blips(object):
"""A dictionary-like object containing the blips, keyed on blip ID."""
def get(self, blip_id, default_value=None):
"""Retrieves a blip.
Returns:
A Blip object. If none found for the ID, it returns None,
or if default_value is specified, it returns that.
"""
return self._blips.get(blip_id, default_value)
...
blip.py
sphinx-build
-b html
-d _build/doctrees
. _build/html
+
+
Makefile
=
index.html
Documentation
Events -> Operations
def process_events(self, json):
parsed = simplejson.loads(json)
pending_ops = ops.OperationQueue()
event_wavelet = self._wavelet_from_json(parsed, pending_ops)
for event_data in parsed['events']:
for payload in self._handlers.get(event_data['type'], []):
handler, event_class, context, filter = payload
event = event_class(event_data, event_wavelet)
handler(event, event_wavelet)
return simplejson.dumps(pending_ops.serialize())
appengine_robot_runner.py
app.yaml
main.py
class RobotEventHandler(webapp.RequestHandler):
def __init__(self, robot):
self._robot = robot
def post(self):
json_body = self.request.body
json_body = unicode(json_body, 'utf8')
json_response = self._robot.process_events(json_body)
self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
self.response.out.write(json_response.encode('utf-8'))
def create_robot_webapp(robot):
return webapp.WSGIApplication([
('.*/_wave/robot/profile',
lambda: ProfileHandler(
robot.profile_json,
'application/json')),
('.*/_wave/capabilities.xml',
lambda: CapabilitiesHandler(
robot.capabilities_xml,
'application/xml')),
('.*/_wave/robot/jsonrpc',
lambda: RobotEventHandler(robot)),
('.*/_wave/verify_token',
lambda: RobotVerifyTokenHandler(robot)),
])
def run(robot):
app = create_robot_webapp(robot)
run_wsgi_app(app)
robot.py
application: emo-bot
version: 1
runtime: python
handlers:
- url: /_wave/.*
script: main.py
- url: /public/(.*)
static_files: public/\1
upload: public/(.*)
import logging
from waveapi import appengine_robot_runner
from waveapi import element
from waveapi import events
from waveapi import ops
from waveapi import robot
anim_base = 'http://emo-bot.appspot.com'
anim_ext = '.gif'
emoticons = {
':)' : 'smile',
':(' : 'frown',
'(heart)': 'heart'
}
def ProcessBlip(event, wavelet):
blip = event.blip
for emoticon in emoticons:
r = blip.all(emoticon)
if r:
r.replace(element.Image(anim_base + emoticons[emoticon] + anim_ext))
pass
if __name__ == '__main__':
emobot = robot.Robot('Emo Bot',
image_url='http://emo-bot.appspot.com/public/thumbnail.png',
profile_url='http://emo-bot.appspot.com/')
emobot.register_handler(events.BlipSubmitted, ProcessBlip)
emobot.register_handler(events.WaveletSelfAdded, ProcessBlip)
appengine_robot_runner.run(emobot, debug=True)
Incoming JSON
{"params": {
"blipId": "b+FYYeTpCXJ",
"waveletId": "wavesandbox.com!conv+root",
"waveId": "wavesandbox.com!w+FYYeTpCXI",
"modifyAction": {
"modifyHow": "REPLACE",
"elements": [
{
"type": "IMAGE",
"properties": {
"url": "http://wave-skimmy.appspot.com/smile.gif"
}
}
]
},
"modifyQuery": {
"textMatch": ":)",
"maxRes": -1
}
},
"method": "document.modify",
"id": "op2"
}
"events": [
{
"type": "WAVELET_SELF_ADDED",
"modifiedBy": "pamela.fox@wavesandbox.com",
"timestamp": 1269400482868,
"properties": {
"blipId": "b+FYYeTpCXJ"
}
}
],
"wavelet": {
"creationTime": 1269400451853,
"lastModifiedTime": 1269400482868,
"version": 14,
"participants": [
"pamela.fox@wavesandbox.com",
"wave-skimmy@appspot.com"
],
"creator": "pamela.fox@wavesandbox.com",
"rootBlipId": "b+FYYeTpCXJ",
"title": "Dear Diary,",
"waveId": "wavesandbox.com!w+FYYeTpCXI",
"waveletId": "wavesandbox.com!conv+root"
},
Outgoing JSON
Google Wave
Behind the Scenes
Google Wave Robots API
Examples
pamelafox@
@pamelafox
skip 3, delete 1
skip 1, delete 1
skip 1, delete 1
skip 2, delete 1
Ferry
Tasky
Monty+Syntaxy
Poker
http://emo-bot.appspot.com/_wave/robot/jsonrpc
http://emo-bot.appspot.com/_wave/robot/jsonrpcMore presentations by Pamela Fox
Popular presentations
13 Consejos para celebrar el Año Nuevo chino
Estampas Multimedia on
Este gran año corresponde al Dragón de Agua y se celebra desde el 23 de enero de 2012. Las ceremonias por el feng shui tienen ...
Trello Architecture
Brett Kiefer on
This is the visual part of a talk I gave on the trello.com architecture at the MongoDB user group on 18 Jan 2012. Blog post ...
More popular prezis in Explore>