blob: cdd0d469b62f4f7b3ae03a872bb62be4fd1bc313 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Channel API.
This module allows App Engine apps to push messages to a client.
Functions defined in this module:
create_channel: Creates a channel to send messages to.
send_message: Send a message to any clients listening on the given channel.
"""
import os
from google.appengine.api import api_base_pb
from google.appengine.api import apiproxy_stub_map
from google.appengine.api.channel import channel_service_pb
from google.appengine.runtime import apiproxy_errors
MAXIMUM_CLIENT_ID_LENGTH = 256
MAXIMUM_TOKEN_DURATION_MINUTES = 24 * 60
MAXIMUM_MESSAGE_LENGTH = 32767
class Error(Exception):
"""Base error class for this module."""
class InvalidChannelClientIdError(Error):
"""Error that indicates a bad client id."""
class InvalidChannelTokenDurationError(Error):
"""Error that indicates the requested duration is invalid."""
class InvalidMessageError(Error):
"""Error that indicates a message is malformed."""
class AppIdAliasRequired(Error):
"""Error that indicates you must assign an application alias to your app."""
def _ToChannelError(error):
"""Translate an application error to a channel Error, if possible.
Args:
error: An ApplicationError to translate.
Returns:
The appropriate channel service error, if a match is found, or the original
ApplicationError.
"""
error_map = {
channel_service_pb.ChannelServiceError.INVALID_CHANNEL_KEY:
InvalidChannelClientIdError,
channel_service_pb.ChannelServiceError.BAD_MESSAGE:
InvalidMessageError,
channel_service_pb.ChannelServiceError.APPID_ALIAS_REQUIRED:
AppIdAliasRequired
}
if error.application_error in error_map:
return error_map[error.application_error](error.error_detail)
else:
return error
def _GetService():
"""Gets the service name to use, based on if we're on the dev server."""
server_software = os.environ.get('SERVER_SOFTWARE', '')
if (server_software.startswith('Devel') or
server_software.startswith('test')):
return 'channel'
else:
return 'xmpp'
def _ValidateClientId(client_id):
"""Validates a client id.
Args:
client_id: The client id provided by the application.
Returns:
If the client id is of type str, returns the original client id.
If the client id is of type unicode, returns the id encoded to utf-8.
Raises:
InvalidChannelClientIdError: if client id is not an instance of str or
unicode, or if the (utf-8 encoded) string is longer than 64 characters.
"""
if not isinstance(client_id, basestring):
raise InvalidChannelClientIdError('"%s" is not a string.' % client_id)
if isinstance(client_id, unicode):
client_id = client_id.encode('utf-8')
if len(client_id) > MAXIMUM_CLIENT_ID_LENGTH:
msg = 'Client id length %d is greater than max length %d' % (
len(client_id), MAXIMUM_CLIENT_ID_LENGTH)
raise InvalidChannelClientIdError(msg)
return client_id
def create_channel(client_id, duration_minutes=None):
"""Create a channel.
Args:
client_id: A string to identify this channel on the server side.
duration_minutes: An int specifying the number of minutes for which the
returned token should be valid.
Returns:
A token that the client can use to connect to the channel.
Raises:
InvalidChannelClientIdError: if clientid is not an instance of str or
unicode, or if the (utf-8 encoded) string is longer than 64 characters.
InvalidChannelTokenDurationError: if duration_minutes is not a number, less
than 1, or greater than 1440 (the number of minutes in a day).
Other errors returned by _ToChannelError
"""
client_id = _ValidateClientId(client_id)
if not duration_minutes is None:
if not isinstance(duration_minutes, (int, long)):
raise InvalidChannelTokenDurationError(
'Argument duration_minutes must be integral')
elif duration_minutes < 1:
raise InvalidChannelTokenDurationError(
'Argument duration_minutes must not be less than 1')
elif duration_minutes > MAXIMUM_TOKEN_DURATION_MINUTES:
msg = ('Argument duration_minutes must be less than %d'
% (MAXIMUM_TOKEN_DURATION_MINUTES + 1))
raise InvalidChannelTokenDurationError(msg)
request = channel_service_pb.CreateChannelRequest()
response = channel_service_pb.CreateChannelResponse()
request.set_application_key(client_id)
if not duration_minutes is None:
request.set_duration_minutes(duration_minutes)
try:
apiproxy_stub_map.MakeSyncCall(_GetService(),
'CreateChannel',
request,
response)
except apiproxy_errors.ApplicationError, e:
raise _ToChannelError(e)
return response.token()
def send_message(client_id, message):
"""Send a message to a channel.
Args:
client_id: The client id passed to create_channel.
message: A string representing the message to send.
Raises:
InvalidChannelClientIdError: if client_id is not an instance of str or
unicode, or if the (utf-8 encoded) string is longer than 64 characters.
InvalidMessageError: if the message isn't a string or is too long.
Errors returned by _ToChannelError
"""
client_id = _ValidateClientId(client_id)
if isinstance(message, unicode):
message = message.encode('utf-8')
elif not isinstance(message, str):
raise InvalidMessageError('Message must be a string')
if len(message) > MAXIMUM_MESSAGE_LENGTH:
raise InvalidMessageError(
'Message must be no longer than %d chars' % MAXIMUM_MESSAGE_LENGTH)
request = channel_service_pb.SendMessageRequest()
response = api_base_pb.VoidProto()
request.set_application_key(client_id)
request.set_message(message)
try:
apiproxy_stub_map.MakeSyncCall(_GetService(),
'SendChannelMessage',
request,
response)
except apiproxy_errors.ApplicationError, e:
raise _ToChannelError(e)