Skip to content

Instantly share code, notes, and snippets.

@WorldMaker
Created September 23, 2009 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save WorldMaker/192277 to your computer and use it in GitHub Desktop.
Save WorldMaker/192277 to your computer and use it in GitHub Desktop.
'Dirty' combined IM and SMS requests framework for AppEngine
#
# textapp -- Combined SMS and IM dispatching
# Copyright 2009 Max Battcher. All Rights Reserved.
#
# Microsoft Public License (Ms-PL)
#
# This license governs use of the accompanying software. If you use the
# software, you accept this license. If you do not accept the license,
# do not use the software.
#
# 1. Definitions
#
# The terms “reproduce,” “reproduction,” “derivative works,” and
# “distribution” have the same meaning here as under U.S. copyright
# law. A “contribution” is the original software, or any additions or
# changes to the software. A “contributor” is any person that
# distributes its contribution under this license. “Licensed patents”
# are a contributor’s patent claims that read directly on its
# contribution.
#
# 2. Grant of Rights
#
# (A) Copyright Grant- Subject to the terms of this license, including
# the license conditions and limitations in section 3, each contributor
# grants you a non-exclusive, worldwide, royalty-free copyright license
# to reproduce its contribution, prepare derivative works of its
# contribution, and distribute its contribution or any derivative works
# that you create.
#
# (B) Patent Grant- Subject to the terms of this license, including the
# license conditions and limitations in section 3, each contributor
# grants you a non-exclusive, worldwide, royalty-free license under its
# licensed patents to make, have made, use, sell, offer for sale,
# import, and/or otherwise dispose of its contribution in the software
# or derivative works of the contribution in the software.
#
# 3. Conditions and Limitations
#
# (A) No Trademark License- This license does not grant you rights to
# use any contributors’ name, logo, or trademarks.
#
# (B) If you bring a patent claim against any contributor over patents
# that you claim are infringed by the software, your patent license from
# such contributor to the software ends automatically.
#
# (C) If you distribute any portion of the software, you must retain all
# copyright, patent, trademark, and attribution notices that are present
# in the software.
#
# (D) If you distribute any portion of the software in source code form,
# you may do so only under this license by including a complete copy of
# this license with your distribution. If you distribute any portion of
# the software in compiled or object code form, you may only do so under
# a license that complies with this license.
#
# (E) The software is licensed “as-is.” You bear the risk of using it.
# The contributors give no express warranties, guarantees or conditions.
# You may have additional consumer rights under your local laws which
# this license cannot change. To the extent permitted under your local
# laws, the contributors exclude the implied warranties of
# merchantability, fitness for a particular purpose and
# non-infringement.
#
from google.appengine.api import xmpp
from google.appengine.ext.webapp.xmpp_handlers import BaseHandler
from models import Player
from webappfb import FacebookRequestHandler
import logging
import re
class Error(Exception):
"""Text application error base class."""
pass
class NoHandlerError(Error):
"""No matching regex/handler error."""
pass
class UnregisteredJidError(Error):
"""Unregisted JID."""
pass
# Based on, for instance, Django's RegexURLPattern or webapps WSGIApplication
class TextApplication(object):
def __init__(self, mapping):
compiled = []
for regex, handler in mapping:
if not regex.startswith('^'):
regex = '^' + regex
if not regex.endswith('$'):
regex = regex + '$'
compiled.append((re.compile(regex, re.IGNORECASE | re.UNICODE),
handler))
self._mapping = compiled
def __call__(self, message):
for regex, handler in self._mapping:
match = regex.match(message.body.strip())
if match:
# If the groups are named, use kwargs, otherwise args
args, kwargs = (), match.groupdict()
if not kwargs:
args = match.groups()
return handler(message, *args, **kwargs)
raise NoHandlerError
# Borrowed from google.appengine.api.xmpp, replaced __'s for subclasses
class Message(object):
"""Encapsulates an XMPP message received by the application."""
def __init__(self, vars):
"""Constructs a new XMPP Message from an HTTP request.
Args:
vars: A dict-like object to extract message arguments from.
"""
try:
self._sender = vars["from"]
self._to = vars["to"]
self._body = vars["body"]
except KeyError, e:
raise xmpp.InvalidMessageError(e[0])
self._command = None
self._arg = None
@property
def sender(self):
return self._sender
@property
def to(self):
return self._to
@property
def body(self):
return self._body
def __parse_command(self):
if self._arg != None:
return
body = self._body
if body.startswith('\\'):
body = '/' + body[1:]
self._arg = ''
if body.startswith('/'):
parts = body.split(' ', 1)
self._command = parts[0][1:]
if len(parts) > 1:
self._arg = parts[1].strip()
else:
self._arg = self._body.strip()
@property
def command(self):
self._parse_command()
return self._command
@property
def arg(self):
self._parse_command()
return self._arg
def reply(self, body, message_type=xmpp.MESSAGE_TYPE_CHAT, raw_xml=False,
send_message=xmpp.send_message):
"""Convenience function to reply to a message.
Args:
body: str: The body of the message
message_type, raw_xml: As per send_message.
send_message: Used for testing.
Returns:
A status code as per send_message.
Raises:
See send_message.
"""
return send_message([self.sender], body, from_jid=self.to,
message_type=message_type, raw_xml=raw_xml)
class FacebookXmppMessage(Message):
def __init__(self, vars, facebook):
self._facebook = facebook
super(FacebookXmppMessage, self).__init__(vars)
barejid = self._sender
slash = barejid.find('/')
if slash >= 0:
barejid = barejid[:slash]
players = list(Player.all().filter('jid =', barejid))
self._senderuid = None
if len(players) == 1:
self._senderuid = players[0].uid
@property
def facebook(self):
return self._facebook
@property
def senderuid(self):
if self._senderuid is None:
raise UnregisteredJidError
return self._senderuid
class FacebookSmsMessage(FacebookXmppMessage):
def __init__(self, vars, facebook):
self._facebook = facebook
self._sender = 'facebook-sms'
self._to = 'facebook-sms'
try:
self._sid = vars['fb_sig_sms_sid']
self._senderuid = vars['fb_sig_user']
self._body = vars['fb_sig_message']
except KeyError, e:
raise xmpp.InvalidMessageError(e[0])
self._command = None
self._arg = None
def reply(self, body, **kwargs):
return self.facebook.sms.send(self._senderuid,
body,
self._sid,
False,
)
class XmppHandler(BaseHandler):
def message_received(self, message):
self.application(message)
class FacebookXmppHandler(FacebookRequestHandler):
def handle_exception(self, exception, debug_mode):
if self.message:
if isinstance(exception, UnregisteredJidError):
self.message.reply("""You need to register first:
http://apps.facebook.com/enlark-assassins/my/settings""")
elif isinstance(exception, NoHandlerError):
self.message.reply("Unrecognized command.")
else:
self.message.reply('An error occurred processing your message.')
else:
super(FacebookXmppHandler, self).handle_exception(exception, debug_mode)
def post(self):
if self.redirecting: return # Unlikely, but...
try:
self.message = FacebookXmppMessage(self.request.POST,
self.facebook)
except xmpp.InvalidMessageError, e:
logging.error("Invalid XMPP request: %s", e[0])
return
reply = self.application(self.message)
if reply:
self.message.reply(reply)
class FacebookSmsHandler(FacebookRequestHandler):
def canvas(self):
raise NotImplementedError()
def handle_exception(self, exception, debug_mode):
if self.message:
if isinstance(exception, NoHandlerError):
self.message.reply('Unrecognized command.')
else:
self.message.reply('An error occurred processing your message.')
else:
super(FacebookSmsHandler, self).handle_exception(exception, debug_mode)
def post(self):
if self.redirecting: return
sms = self.request.get('fb_sig_sms')
if sms and int(sms) == 1:
try:
self.message = FacebookSmsMessage(self.request.POST,
self.facebook)
except xmpp.InvalidMessageError, e:
logging.error("Invalid SMS request: %s", e[0])
return
reply = self.application(self.message)
if reply:
self.message.reply(reply)
else:
self.canvas()
# vim: ai et ts=4 sts=4 sw=4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment