Server-side Implementation

Development Environment

To implement a simple server, we’ll be using Google App Engine + Python in this example. Please refer to the docs here.

User Model

The user model stores basic information to manage users. It has information needed for signup, chat, and a few device-related information.

# models.py

class User(ndb.Model):
  email = ndb.StringProperty(required=True, indexed=True)
  nickname = ndb.StringProperty(required=True, indexed=True)
  password = ndb.StringProperty(required=True)
  session = ndb.StringProperty(required=True)
  sendbird_id = ndb.StringProperty(required=True)
  created_time = ndb.DateTimeProperty(auto_now_add=True)
  updated_time = ndb.DateTimeProperty(auto_now=True)
  device_token = ndb.StringProperty(required=True, indexed=True)
  device_type = ndb.StringProperty(required=True, choices=set(["IOS", "ANDROID", "WEB"]))

Each field are defined and used as the following:

  • email

This is used for logging in. All users must have unique email addresses.

  • password

    Password is used for login. In this example, we’ll use a simple SHA256 hash to store encrypted passwords.

  • nickname

    Nickname is the display name used during chat. The initial value will be set to the local part of the email address. Users can change their nickname later.

  • session

    Session key used between the client and the server.

  • sendbird_id

    This is the key used to uniquely identify users during chat. A unique value needs to be assigned to the users. In this example, we’ll simply use the email addresses, but in production, it is recommended to use a different value.

  • device_token, device_push

    Data used for push notifications. In this example, we won’t be building a push notification feature.

User Signup Process

Let’s implement the request handler for the user signup process. Here, we’ll be accepting user data using JSON format through the body within POST request.

API

POST /signup

JSON

{
  “email”: “[email protected]”,
  “nickname”: “USER_NICKNAME”,
  “password”: “USER_PASSWORD”,
  “device_token”: “DEVICE_TOKEN”,
  “device_type”: “IOS”, “ANDROID” or “WEB”
}

Request Handler

# main.py

class UserSignUp(webapp2.RequestHandler):
  def post(self):
    data = json.loads(self.request.body)
    check = User.query(User.email == data['email']).fetch(1)
    if len(check) > 0:
      logging.info("The email exists.")
      self.response.write(json.dumps({
          "result": "error",
          "message": "The email exists."
          }))
      return

    user = User()
    try:
      user.email = data['email']
      user.nickname = data['nickname']
      user.device_token = data['device_token']
      user.device_type = data['device_type']
      user.session = uuid.uuid4().hex
      user.sendbird_id = uuid.uuid4().hex
      user.password = User.password_encrypt(data['password'])
      user.put()
    except:
      logging.info("You cannot sign up.")
      self.response.write(json.dumps({
          "result": "error",
          "message": "You cannot sign up."
          }))
      return

    user_dict = user.to_dict()
    self.response.write(json.dumps({
        "result": "success",
        "user": user_dict
        }))

User Login Process

Now, we’ll implement a request handler to process user login. We’ll be accepting JSON formatted data (email, password, device token, device type, etc.) through the body of POST request. We’ll look up a user with matching email and password, then return the user data.

API

POST /signin

JSON

{
  “email”: “[email protected]”,
  “password”: “USER_PASSWORD”,
  “device_token”: “DEVICE_TOKEN”,
  “device_type”: “IOS”, “ANDROID” or “WEB”
}

Request Handler

# main.py

class UserSignIn(webapp2.RequestHandler):
  def post(self):
    data = json.loads(self.request.body)
    user_list = User.query(User.email == data['email']).fetch(1)
    if len(user_list) > 0:
      user = user_list[0]
      if user.password == User.password_encrypt(data['password']):
        user.device_token = data['device_token']
        user.device_type = data['device_type']
        user.put()
        self.response.write(json.dumps({
            "result": "success",
            "user": user.to_dict()
            }))
        return
      else:
        self.response(json.dumps({
            "result": "error",
            "message": "Password is incorrect."
            }))
        return
    else:
      self.response(json.dumps({
          "result": "error",
          "message": "User not found."
          }))
      return

YouTube Video Model

The YouTube model is used to submit and manage videos. The class has the following properties:

class YouTube(ndb.Model):
  url = ndb.StringProperty(required=True)
  video_id = ndb.StringProperty(required=True)
  title = ndb.StringProperty(required=True)
  thumbnail = ndb.StringProperty(required=True)
  owner = ndb.IntegerProperty(required=True, indexed=True)
  created_time = ndb.DateTimeProperty(auto_now_add=True, indexed=True)
  updated_time = ndb.DateTimeProperty(auto_now=True)
  channel_url = ndb.StringProperty(required=True)
  viewer = ndb.IntegerProperty(default=0)

Each field are defined and used as the following:

  • url

    The URL of YouTube video

  • video_id

    The unique ID of the video

  • title

    The title of the submitted video

  • thumbnail

    The thumbnail image of the submitted video

  • channel_url

    The public chat channel URL assigned for the video

  • viewer

    The number of times the video was viewed on BirdTube app

Submitting YouTube Video

Since BirdTube is relying on users’ submissions, we need to implement a request handler to process videos submitted. A user needs to login first to submit, and that user will receive Session from the server. The client will send this value along with the submission request. The data required for the submission gets transferred in JSON format through the body of POST request. The server will then create a public chat channel using SendBird’s server API, and store the channel URL of the chat room within YouTube model.

You will need an API Token to use SendBird’s server API. If you haven’t already, head over to the dashboard to retrieve API Token for your app.

Refer to the server API docs to learn more about creating open chat channels.

API

POST /video/register

JSON

{
  "session": "USER_SESSION",
  "video_id": "YOUTUBE_VIDEO_ID",
  "url": "YOUTUBE_VIDEO_URL",
  "title": "YOUTUBE_VIDEO_TITLE",
  "thumbnail": "YOUTUBE_VIDEO_THUMBNAIL_URL",
}

Request Handler

class RegisterVideo(webapp2.RequestHandler):
  def post(self):
    data = json.loads(self.request.body)
    youtube = YouTube()
    session = data['session']
    user_list = User.query(User.session == session).fetch(1)
    if len(user_list) > 0:
      owner = user_list[0]

    try:
      youtube_list = YouTube.query(YouTube.video_id == data['video_id']).fetch(1)
      if len(youtube_list) > 0:
        self.response.write(json.dumps({
            "result": "error",
            "message": "The video is already registered."
            }))
        logging.info("The video is already registered.")
        return

      youtube.url = data['url']
      youtube.video_id = data['video_id']
      youtube.title = data['title']
      youtube.owner = owner.key.id()
      youtube.thumbnail = data['thumbnail']

      create_channel_api = "https://api.sendbird.com/channel/create"
      api_token = "<YOUR_APP_API_TOKEN>"
      request_body = {
          'auth': api_token,
          'channel_url': uuid.uuid4().hex,
          'name': data['title'],
          'cover_url': data['thumbnail'],
          'data': ""
          }
      data = json.dumps(request_body)
      req = urllib2.Request(create_channel_api, data, {'Content-Type': 'application/json'})
      f = urllib2.urlopen(req)
      response = f.read()
      logging.info(response)
      channel = json.loads(response)
      channel_url = channel['channel_url']
      youtube.channel_url = channel_url
      youtube.put()
    except:
      logging.info("error")
      self.response.write(json.dumps({
          "result": "error",
          "message": "The video can't be registered."
          }))
      return
    logging.info("success")
    self.response.write(json.dumps({
        "result": "success",
        }))

Browsing the List of Submitted Videos

The users will want to see the list of submitted videos and chat rooms. We will allow users who have not yet signed up browse the videos to get them engaged first. Because of this, we won’t be checking the session here. Since there can be a lot of videos, we’ll implement a simple pagination using number of videos to show per page, current page offset, and sorting condition. These information will be received using JSON format through the body of the POST request.

API

POST /video/list

JSON

{
  "offset": LIST_OFFSET,
  "limit": NUMBER_OF_VIDEOS_PER_PAGE,
  "order_by": 1(popular) or 2(new)
}

Request Handler

class VideoListLoading(webapp2.RequestHandler):
  def post(self):
    data = json.loads(self.request.body)
    offset = data['offset']
    limit = data['limit']
    order_by = data['order_by']

    video_list = []
    if order_by == 1:
      video_list = YouTube.query().order(-YouTube.viewer).fetch(limit, offset=offset)
    else:
      video_list = YouTube.query().order(-YouTube.created_time).fetch(limit, offset=offset)

    video_dict_list = []
    for video in video_list:
      video_dict_list.append(video.to_dict())

    self.response.write(json.dumps({
        "result": "success",
        "video_list": video_dict_list
        }))

Viewing the Video and Chatting

When a user starts to watch the video, a chat room will be displayed at the same time. Viewing the video will increase the viewers count. We will be using the number of views to rank videos by popularity and display them on the “Popular” tab. The video ID will be received in JSON format through the body of the POST request.

API

POST /video/view

JSON

{
  “video_id”: “YOUTUBE_VIDEO_ID”
}

Request Handler

class ViewVideo(webapp2.RequestHandler):
  def post(self):
    data = json.loads(self.request.body)
    video_id = data['video_id']
    video_list = YouTube.query(YouTube.video_id == video_id).fetch(1)
    if len(video_list) > 0:
      video = video_list[0]
      video.viewer = video.viewer + 1
      video.put()

    self.response.write("{}")