Simple chat using Channel API on Google App Engine for Python

I present to you a free translation of an article entitled " A Simple Chat using the Channel API ". I also decided to add my code a bit.

Today we present to you a new article for the Google App Engine devoted to the Channel API, which appeared in December 2010 in release 1.4. From this moment, it became possible to send messages directly from the server to the client and back without using polling .
Therefore, it became quite simple to implement chat on Google App Engine. The implementation process is described under the cut.

You can watch the demo at http://chat-channelapi.appspot.com/ .
The project code can be downloaded here.(the code from the original article is here ).

In our application, in order to create a channel between the user and the program, you need to take the following steps, shown below in the picture.
image
Steps:
1) The application creates the channel id (channel id) and token and sends them to the client
2) The client uses the token to open the socket that the channel will listen to
3) Client 2 sends a chat message to the application, along with its unique channel id
4 ) The application sends a message to all clients who listen to the channel through the socket. For this, the channel id of each client is used.

Before all steps are described, it should be noted that we have simplified as much as possible the entity of the database that will participate in the program. We created two - the User model and Message.
class OnlineUser(db.Model):
    nick=db.StringProperty(default="")
    channel_id=db.StringProperty(default="")

class Message(db.Model):
    text=db.StringProperty(default="")
    user=db.ReferenceProperty(User)

The code also uses the session mechanism implemented in the GAE utilities library . But do not pay much attention to the session.

Step 1.

In this step, our chat application creates the channel id and token and sends them to the client. The code for this step is quite simple. Just remember to import the Channel API:
from google.appengine.api import channel

After that, create a handler that generates a unique id for each user (we will use the uuid4 () function from the uuid module). The following handler does just that and passes the data to the template to the client:
class ChatHandler(webapp.RequestHandler):
    def get(self):
        self.redirect('/')
        
    def post(self):
        # сессия из библиотеки http://gaeutilities.appspot.com/
        self.session = Session()
        # получаем ник
        nick = self.request.get('nick')
        if not nick:
            self.redirect('/')
        # проверяем, не существует ли пользователя с таким ником
        user = OnlineUser.all().filter('nick =', nick).get()
        if user:
            self.session['error']='That nickname is taken'
            self.redirect('/')
            return
        else:
            self.session['error']=''
        # генерируем уникальный id канала для Channel API
        channel_id=str(uuid.uuid4())
        chat_token = channel.create_channel(channel_id)
        # сохраняем пользователя 
        user = OnlineUser(nick=nick,channel_id=channel_id)
        user.put()
        # получаем последние 100 сообщений
        messages=Message.all().order('date').fetch(1000)
        # генерируем шаблон и отправляем его в качестве ответа клиенту
        template_vars={'nick':nick,'messages':messages,'channel_id':channel_id,'chat_token':chat_token}
        temp = os.path.join(os.path.dirname(__file__),'templates/chat.html')
        outstr = template.render(temp, template_vars)
        self.response.out.write(outstr)

In order not to overload this article, I do not provide the template code here. You can see it on github.

Step 2

Now the client is responsible for extracting the token and opening the socket. We use jQuery to reduce javascript code. Below is our code:
var chat_token = $('#channel_api_params').attr('chat_token');
var channel = new goog.appengine.Channel(chat_token);
var socket = channel.open();
socket.onopen = function(){
};
socket.onmessage = function(m){
  var data = $.parseJSON(m.data);
  $('#center').append(data['html']);
  $('#center').animate({scrollTop: $("#center").attr("scrollHeight")}, 500);
};
  socket.onerror =  function(err){
  alert("Error => "+err.description);
};
  socket.onclose =  function(){
  alert("channel closed");
};

Step 3

At this step, Client 2 sends a message to our chat via the interface. To do this, just make a text box and a button to send a message. The message will be sent in javascript code using a simple listener, which we implement using jQuery. You can use any javsctipt library instead, or simply using the XMLHttpRequest object. Just keep in mind that you must send a unique client channel id to correctly identify the client in the application.
$('#send').click(function(){
  var text = $('#text').val();
  var nick = $('#nick').attr('value');
  var channel_id = $('#channel_api_params').attr('channel_id');
  $.ajax({
    url: '/newMessage/',
    type: 'POST',
    data:{
      text:text,
      nick:nick,
      channel_id:channel_id,
    },
    success: function(data){
    },
    complete:function(){ 
        }      
  });
});

Step 4

In order to receive messages from clients, we need to implement a new handler, which will also send a message to all clients.
class NewMessageHandler(webapp.RequestHandler):    
    def post(self):
        # получаем параметры       
        text = self.request.get('text')
        channel_id = self.request.get('channel_id')        
        q = db.GqlQuery("SELECT * FROM OnlineUser WHERE channel_id = :1", channel_id)
        nick = q.fetch(1)[0].nick    
        date = datetime.datetime.now()
        # сохраняем сообщение
        message=Message(user=nick,text=strip_tags(text), date = date, date_string = date.strftime("%H:%M:%S"))
        message.put()
        # генерируем шаблон сообщения
        messages=[message]
        template_vars={'messages':messages}
        temp = os.path.join(os.path.dirname(__file__),'templates/messages.html')
        outstr = template.render(temp, template_vars)
        channel_msg = json.dumps({'success':True,"html":outstr})
        # отправляем всем клиентам сообщение
        users = OnlineUser.all().fetch(100)        
        for user in users:                        
            channel.send_message(user.channel_id, channel_msg)

An additional step

At this stage, the original article ends, but I wanted to make some changes to the code related to the next task. In the original article, user names are blocked after entering the chat, and you can’t log in using this nickname. We remove this restriction. To do this, delete the user data from the database after the key expires. The key expires, either until two hours have passed, or until the client calls the close () function on the socket. Then the handler registered at / _ah / channel / disconnected / is called. We will write such a handler.
class ChannelDisconnectHandler(webapp.RequestHandler):
    def post(self):
        channel_id = self.request.get('from')
        q = OnlineUser.all().filter('channel_id =', channel_id)
        users = q.fetch(1000)  
        db.delete(users)

In the javascript code, add the processing of the event that occurs when the user leaves this page:
$(window).unload(function (){
    socket.close();        
});

It remains to handle the following situation. If the user entered the chat, but quickly closed the window, then the channel does not open. This leads to the situation that the user record is in the database, but it is not deleted due to the fact that the channel is not closing. Change our user data model:
class OnlineUser(db.Model):
  nick=db.StringProperty(default="")
  channel_id=db.StringProperty(default="")
  creation_date=db.DateTimeProperty(auto_now_add=True)
  opened_socket=db.BooleanProperty(default=False)

Now we have the time to create the record (creation_date) and information about whether the confirmation from the client about the opening of the channel (opened_socket) has come. When a channel is opened on the client side by calling channel.open () on the server side, the handler registered at / _ah / channel / connected / is called. This handler will expose the user with confirmation of the opening of the channel:
class ChannelConnectHandler(webapp.RequestHandler):
    def post(self):
        channel_id = self.request.get('from')
        q = OnlineUser.all().filter('channel_id =', channel_id)
        user = q.fetch(1)[0]
        user.opened_socket = True
        user.put()

This code sends the channel id to the server for identification. The handler is presented below:
class RegisterOpenSocketHandler(webapp.RequestHandler):    
    def post(self):
        channel_id = self.request.get('channel_id')    
        q = OnlineUser.all().filter('channel_id =', channel_id)
        user = q.fetch(1)[0]
        user.opened_socket = True
        user.put() 

The last step will be to start using the cron handler, which will select all entries from the OnlineUser user model who will have confirmation of the channel opening and the creation time from the current one will be more than 120 seconds:
class ClearDBHandler(webapp.RequestHandler):    
    def get(self):
        q = OnlineUser.all().filter('opened_socket =', False)
        users = q.fetch(1000)
        for user in users:
            if ((datetime.datetime.now() - user.creation_date).seconds > 120):
                db.delete(user)

As a result,

we were able to build a simple chat application. Four steps from the original article are enough to show the Channel API, adding to the code was done to make the application look, in my opinion, more complete.

PS The real work of the application showed that it is necessary to filter messages, excluding html tags from them. To do this, import the strip_tags function from the django framework:
from django.utils.html import strip_tags

In the new message handler (NewMessageHandler), we replace the code for creating a new message with the following:
message=Message(user=nick,text=strip_tags(text), date = date, date_string = date.strftime("%H:%M:%S"))

Also popular now: