vj4/app.py

Summary

Maintainability
A
2 hrs
Test Coverage
import asyncio
import datetime
import logging
from os import path

import aiohttp_sentry
import sockjs
from aiohttp import web

from vj4 import db
from vj4 import error
from vj4.model import system
from vj4.service import bus
from vj4.service import smallcache
from vj4.service import staticmanifest
from vj4.util import json
from vj4.util import options
from vj4.util import tools

options.define('debug', default=False, help='Enable debug mode.')
options.define('static', default=True, help='Serve static files.')
options.define('ip_header', default='', help='Header name for remote IP.')
options.define('unsaved_session_expire_seconds', default=43200,
               help='Expire time for unsaved session, in seconds.')
options.define('saved_session_expire_seconds', default=2592000,
               help='Expire time for saved session, in seconds.')
options.define('cookie_domain', default='', help='Cookie domain.')
options.define('cookie_secure', default=False, help='Enable secure cookie flag.')
options.define('registration_token_expire_seconds', default=86400,
               help='Expire time for registration token, in seconds.')
options.define('lostpass_token_expire_seconds', default=3600,
               help='Expire time for lostpass token, in seconds.')
options.define('changemail_token_expire_seconds', default=3600,
               help='Expire time for changemail token, in seconds.')
options.define('url_prefix', default='https://vijos.org', help='URL prefix.')
options.define('cdn_prefix', default='/', help='CDN prefix.')
options.define('sentry_dsn', default='', help='Sentry integration DSN.')

_logger = logging.getLogger(__name__)

WS_MSG_HEARTBEAT = {}


class SentryMiddleware(aiohttp_sentry.SentryMiddleware): # For getting a correct client IP
  async def get_extra_data(self, request):
    return {
      'request': {
        'query_string': request.query_string,
        'headers': dict(request.headers),
        'url': request.path,
        'method': request.method,
        'scheme': request.scheme,
        'env': {
          'REMOTE_ADDR': tools.get_remote_ip(request),
        }
      }
    }


class Application(web.Application):
  def __init__(self):
    middlewares = []
    if options.sentry_dsn:
      middlewares.append(SentryMiddleware({
        'dsn': options.sentry_dsn,
        'environment': 'vj4',
        'debug': options.debug,
      }))

    super(Application, self).__init__(
      debug=options.debug,
      middlewares=middlewares
    )
    globals()[self.__class__.__name__] = lambda: self  # singleton

    static_path = path.join(path.dirname(__file__), '.uibuild')

    # Initialize components.
    staticmanifest.init(static_path)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(db.init())
    loop.run_until_complete(system.setup())
    loop.run_until_complete(system.ensure_db_version())
    loop.run_until_complete(asyncio.gather(tools.ensure_all_indexes(), bus.init()))
    smallcache.init()

    # Load views.
    from vj4.handler import contest
    from vj4.handler import discussion
    from vj4.handler import domain
    from vj4.handler import fs
    from vj4.handler import home
    from vj4.handler import homework
    from vj4.handler import judge
    from vj4.handler import misc
    from vj4.handler import problem
    from vj4.handler import record
    from vj4.handler import training
    from vj4.handler import ranking
    from vj4.handler import user
    from vj4.handler import i18n
    if options.static:
      self.router.add_static('/', static_path, name='static')


def route(url, name, global_route=False):
  def decorate(handler):
    handler.NAME = handler.NAME or name
    handler.TITLE = handler.TITLE or name
    handler.GLOBAL = global_route
    Application().router.add_route('*', url, handler, name=name)
    Application().router.add_route('*', '/d/{domain_id}' + url, handler,
                                   name=name + '_with_domain_id')
    return handler

  return decorate


def connection_route(prefix, name, global_route=False):
  def decorate(conn):
    conn.GLOBAL = global_route
    async def handler(msg, session):
      try:
        if msg.tp == sockjs.MSG_OPEN:
          await session.prepare()
          await session.on_open()
        elif msg.tp == sockjs.MSG_MESSAGE:
          message = json.decode(msg.data)
          if message == WS_MSG_HEARTBEAT:
            pass
          else:
            await session.on_message(**message)
        elif msg.tp == sockjs.MSG_CLOSED:
          await session.on_close()
      except error.UserFacingError as e:
        _logger.warning("Websocket user facing error: %s", repr(e))
        session.close(4000, {'error': e.to_dict()})

    class Manager(sockjs.SessionManager):
      def __init__(self, *args):
        super(Manager, self).__init__(*args)
        self.factory = conn
        self.timeout = datetime.timedelta(seconds=60)

    loop = asyncio.get_event_loop()
    sockjs.add_endpoint(Application(), handler, name=name, prefix=prefix,
                        manager=Manager(name, Application(), handler, loop))
    sockjs.add_endpoint(
        Application(), handler, name=name + '_with_domain_id', prefix='/d/{domain_id}' + prefix,
        manager=Manager(name + '_with_domain_id', Application(), handler, loop))
    return conn

  return decorate