diff --git a/bazarr/get_episodes.py b/bazarr/get_episodes.py index c14fb440a..9b7c2fa91 100644 --- a/bazarr/get_episodes.py +++ b/bazarr/get_episodes.py @@ -25,7 +25,8 @@ def update_all_movies(): def sync_episodes(): - logging.debug('BAZARR Starting episode sync from Sonarr.') + q4ws.append('Episodes sync from Sonarr started...') + logging.debug('BAZARR Starting episodes sync from Sonarr.') apikey_sonarr = settings.sonarr.apikey # Open database connection @@ -41,12 +42,13 @@ def sync_episodes(): episodes_to_add = [] # Get sonarrId for each series from database - seriesIdList = c.execute("SELECT sonarrSeriesId FROM table_shows").fetchall() + seriesIdList = c.execute("SELECT sonarrSeriesId, title FROM table_shows").fetchall() # Close database connection c.close() for seriesId in seriesIdList: + q4ws.append('Getting episodes data for this show: ' + seriesId[1]) # Get episodes data for a series from Sonarr url_sonarr_api_episode = url_sonarr + "/api/episode?seriesId=" + str(seriesId[0]) + "&apikey=" + apikey_sonarr try: @@ -109,4 +111,6 @@ def sync_episodes(): logging.debug('BAZARR All episodes synced from Sonarr into database.') list_missing_subtitles() - logging.debug('BAZARR All missing subtitles updated in database.') \ No newline at end of file + logging.debug('BAZARR All missing subtitles updated in database.') + + q4ws.append('Episodes sync from Sonarr ended.') diff --git a/bazarr/get_movies.py b/bazarr/get_movies.py index f36e5fac1..a9c5f57b1 100644 --- a/bazarr/get_movies.py +++ b/bazarr/get_movies.py @@ -3,6 +3,7 @@ import os import sqlite3 import requests import logging +from queueconfig import q4ws from get_argv import config_dir from config import settings, url_radarr @@ -11,6 +12,7 @@ from list_subtitles import store_subtitles_movie, list_missing_subtitles_movies def update_movies(): + q4ws.append("Update movies list from Radarr is running...") logging.debug('BAZARR Starting movie sync from Radarr.') apikey_radarr = settings.radarr.apikey movie_default_enabled = settings.general.getboolean('movie_default_enabled') @@ -48,6 +50,7 @@ def update_movies(): movies_to_add = [] for movie in r.json(): + q4ws.append("Getting data for this movie: " + movie['title']) if movie['hasFile'] is True: if 'movieFile' in movie: if movie["path"] != None and movie['movieFile']['relativePath'] != None: @@ -125,6 +128,8 @@ def update_movies(): list_missing_subtitles_movies() logging.debug('BAZARR All movie missing subtitles updated in database.') + q4ws.append("Update movies list from Radarr is ended.") + def get_profile_list(): apikey_radarr = settings.radarr.apikey diff --git a/bazarr/get_series.py b/bazarr/get_series.py index 7a68a6205..86d161592 100644 --- a/bazarr/get_series.py +++ b/bazarr/get_series.py @@ -3,6 +3,8 @@ import os import sqlite3 import requests import logging +from queueconfig import q4ws +import datetime from get_argv import config_dir from config import settings, url_sonarr @@ -10,6 +12,7 @@ from list_subtitles import list_missing_subtitles def update_series(): + q4ws.append("Update series list from Sonarr is running...") apikey_sonarr = settings.sonarr.apikey serie_default_enabled = settings.general.getboolean('serie_default_enabled') serie_default_language = settings.general.serie_default_language @@ -50,6 +53,7 @@ def update_series(): series_to_add = [] for show in r.json(): + q4ws.append("Getting series data for this show: " + show['title']) try: overview = unicode(show['overview']) except: @@ -104,6 +108,8 @@ def update_series(): db.commit() db.close() + q4ws.append("Update series list from Sonarr is ended.") + def get_profile_list(): apikey_sonarr = settings.sonarr.apikey diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index c14e41cb8..827826a4d 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -25,6 +25,7 @@ from helper import path_replace, path_replace_movie, path_replace_reverse, \ from list_subtitles import list_missing_subtitles, list_missing_subtitles_movies, store_subtitles, store_subtitles_movie from notifier import send_notifications, send_notifications_movie from utils import history_log, history_log_movie +from queueconfig import q4ws # configure the cache region.configure('dogpile.cache.memory') @@ -425,6 +426,7 @@ def wanted_download_subtitles(path): for i in range(len(attempt)): if attempt[i][0] == language: if search_active(attempt[i][1]) is True: + q4ws.append('Searching ' + str(language_from_alpha2(language)) + ' subtitles for this file: ' + path) message = download_subtitle(path_replace(episode[0]), str(alpha3_from_alpha2(language)), episode[4], providers_list, providers_auth, str(episode[5]), 'series') if message is not None: store_subtitles(path_replace(episode[0])) @@ -466,6 +468,7 @@ def wanted_download_subtitles_movie(path): for i in range(len(attempt)): if attempt[i][0] == language: if search_active(attempt[i][1]) is True: + q4ws.append('Searching ' + str(language_from_alpha2(language)) + ' subtitles for this file: ' + path) message = download_subtitle(path_replace_movie(movie[0]), str(alpha3_from_alpha2(language)), movie[4], providers_list, providers_auth, str(movie[5]), 'movie') if message is not None: store_subtitles_movie(path_replace_movie(movie[0])) diff --git a/bazarr/list_subtitles.py b/bazarr/list_subtitles.py index dde52c3c3..41f5b2005 100644 --- a/bazarr/list_subtitles.py +++ b/bazarr/list_subtitles.py @@ -16,6 +16,8 @@ from config import settings from helper import path_replace, path_replace_movie, path_replace_reverse, \ path_replace_reverse_movie +from queueconfig import q4ws + gc.enable() @@ -23,6 +25,7 @@ def store_subtitles(file): logging.debug('BAZARR started subtitles indexing for this file: ' + file) actual_subtitles = [] if os.path.exists(file): + q4ws.append('Analyzing this file for subtitles: ' + file) if os.path.splitext(file)[1] == '.mkv': logging.debug("BAZARR is trying to index embedded subtitles.") try: @@ -95,6 +98,7 @@ def store_subtitles_movie(file): logging.debug('BAZARR started subtitles indexing for this file: ' + file) actual_subtitles = [] if os.path.exists(file): + q4ws.append('Analyzing this file for subtitles: ' + file) if os.path.splitext(file)[1] == '.mkv': logging.debug("BAZARR is trying to index embedded subtitles.") try: diff --git a/bazarr/logger.py b/bazarr/logger.py index 2e5fd18a8..d5f31abec 100644 --- a/bazarr/logger.py +++ b/bazarr/logger.py @@ -78,6 +78,7 @@ def configure_logging(debug=False): logging.getLogger("guessit").setLevel(logging.WARNING) logging.getLogger("rebulk").setLevel(logging.WARNING) logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL) + logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING) fh.setLevel(log_level) logger.addHandler(fh) diff --git a/bazarr/main.py b/bazarr/main.py index 657afe05b..d4f5c91aa 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -1,4 +1,4 @@ -bazarr_version = '0.6.9.1' +bazarr_version = '0.6.9.5' import gc gc.enable() @@ -18,6 +18,7 @@ from update_db import * from notifier import update_notifier update_notifier() +import queueconfig import logging from logger import configure_logging, empty_log @@ -36,7 +37,7 @@ if settings.proxy.type != 'None': os.environ['HTTPS_PROXY'] = str(proxy) os.environ['NO_PROXY'] = str(settings.proxy.exclude) -from bottle import route, run, template, static_file, request, redirect, response, HTTPError, app, hook +from bottle import route, run, template, static_file, request, redirect, response, HTTPError, app, hook, abort import bottle bottle.TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(__file__), '../views/')) if "PYCHARM_HOSTED" in os.environ: @@ -45,7 +46,24 @@ if "PYCHARM_HOSTED" in os.environ: else: bottle.ERROR_PAGE_TEMPLATE = bottle.ERROR_PAGE_TEMPLATE.replace('if DEBUG and', 'if') -from cherrypy.wsgiserver import CherryPyWSGIServer +# Install gevent under user directory if it'S not already available. This one is required to use websocket. +try: + import gevent +except ImportError as e: + logging.exception('BAZARR require gevent Python module to be installed using pip.') + try: + stop_file = open(os.path.join(config_dir, "bazarr.stop"), "w") + except Exception as e: + logging.error('BAZARR Cannot create bazarr.stop file.') + else: + stop_file.write('') + stop_file.close() + os._exit(0) + +from gevent.pywsgi import WSGIServer +from geventwebsocket import WebSocketError +from geventwebsocket.handler import WebSocketHandler +#from cherrypy.wsgiserver import CherryPyWSGIServer from beaker.middleware import SessionMiddleware from cork import Cork @@ -1732,14 +1750,31 @@ def test_notification(protocol, provider): ) +@route(base_url + 'websocket') +@custom_auth_basic(check_credentials) +def handle_websocket(): + wsock = request.environ.get('wsgi.websocket') + if not wsock: + abort(400, 'Expected WebSocket request.') + + queueconfig.q4ws.clear() + + while True: + try: + if len(queueconfig.q4ws) > 0: + wsock.send(queueconfig.q4ws.popleft()) + gevent.sleep(0) + except WebSocketError: + break + import warnings # Mute DeprecationWarning warnings.simplefilter("ignore", DeprecationWarning) -server = CherryPyWSGIServer((str(settings.general.ip), int(settings.general.port)), app) +server = WSGIServer((str(settings.general.ip), int(settings.general.port)), app, handler_class=WebSocketHandler) try: logging.info('BAZARR is started and waiting for request on http://' + str(settings.general.ip) + ':' + str(settings.general.port) + str(base_url)) # print 'Bazarr is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url) - server.start() + server.serve_forever() except KeyboardInterrupt: shutdown() diff --git a/bazarr/queueconfig.py b/bazarr/queueconfig.py new file mode 100644 index 000000000..7c0fb1efa --- /dev/null +++ b/bazarr/queueconfig.py @@ -0,0 +1,4 @@ +from collections import deque + +global q4ws +q4ws = deque(maxlen=10) \ No newline at end of file diff --git a/libs/geventwebsocket/__init__.py b/libs/geventwebsocket/__init__.py new file mode 100644 index 000000000..5ee3f9610 --- /dev/null +++ b/libs/geventwebsocket/__init__.py @@ -0,0 +1,21 @@ +VERSION = (0, 10, 1, 'final', 0) + +__all__ = [ + 'WebSocketApplication', + 'Resource', + 'WebSocketServer', + 'WebSocketError', + 'get_version' +] + + +def get_version(*args, **kwargs): + from .utils import get_version + return get_version(*args, **kwargs) + +try: + from .resource import WebSocketApplication, Resource + from .server import WebSocketServer + from .exceptions import WebSocketError +except ImportError: + pass diff --git a/libs/geventwebsocket/_compat.py b/libs/geventwebsocket/_compat.py new file mode 100644 index 000000000..70354135b --- /dev/null +++ b/libs/geventwebsocket/_compat.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division, print_function + +import sys +import codecs + + +PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + + +if PY2: + bytes = str + text_type = unicode + string_types = basestring + range_type = xrange + iteritems = lambda x: x.iteritems() + # b = lambda x: x +else: + text_type = str + string_types = str, + range_type = range + iteritems = lambda x: iter(x.items()) + # b = lambda x: codecs.latin_1_encode(x)[0] diff --git a/libs/geventwebsocket/exceptions.py b/libs/geventwebsocket/exceptions.py new file mode 100644 index 000000000..e066727e5 --- /dev/null +++ b/libs/geventwebsocket/exceptions.py @@ -0,0 +1,19 @@ +from socket import error as socket_error + + +class WebSocketError(socket_error): + """ + Base class for all websocket errors. + """ + + +class ProtocolError(WebSocketError): + """ + Raised if an error occurs when de/encoding the websocket protocol. + """ + + +class FrameTooLargeException(ProtocolError): + """ + Raised if a frame is received that is too large. + """ diff --git a/libs/geventwebsocket/gunicorn/__init__.py b/libs/geventwebsocket/gunicorn/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/geventwebsocket/gunicorn/workers.py b/libs/geventwebsocket/gunicorn/workers.py new file mode 100644 index 000000000..d0aa13694 --- /dev/null +++ b/libs/geventwebsocket/gunicorn/workers.py @@ -0,0 +1,6 @@ +from geventwebsocket.handler import WebSocketHandler +from gunicorn.workers.ggevent import GeventPyWSGIWorker + + +class GeventWebSocketWorker(GeventPyWSGIWorker): + wsgi_handler = WebSocketHandler diff --git a/libs/geventwebsocket/handler.py b/libs/geventwebsocket/handler.py new file mode 100644 index 000000000..8aec77c05 --- /dev/null +++ b/libs/geventwebsocket/handler.py @@ -0,0 +1,283 @@ +import base64 +import hashlib + +from gevent.pywsgi import WSGIHandler +from ._compat import PY3 +from .websocket import WebSocket, Stream +from .logging import create_logger + + +class Client(object): + def __init__(self, address, ws): + self.address = address + self.ws = ws + + +class WebSocketHandler(WSGIHandler): + """ + Automatically upgrades the connection to a websocket. + + To prevent the WebSocketHandler to call the underlying WSGI application, + but only setup the WebSocket negotiations, do: + + mywebsockethandler.prevent_wsgi_call = True + + before calling run_application(). This is useful if you want to do more + things before calling the app, and want to off-load the WebSocket + negotiations to this library. Socket.IO needs this for example, to send + the 'ack' before yielding the control to your WSGI app. + """ + + SUPPORTED_VERSIONS = ('13', '8', '7') + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + def run_websocket(self): + """ + Called when a websocket has been created successfully. + """ + + if getattr(self, 'prevent_wsgi_call', False): + return + + # In case WebSocketServer is not used + if not hasattr(self.server, 'clients'): + self.server.clients = {} + + # Since we're now a websocket connection, we don't care what the + # application actually responds with for the http response + + try: + self.server.clients[self.client_address] = Client( + self.client_address, self.websocket) + list(self.application(self.environ, lambda s, h, e=None: [])) + finally: + del self.server.clients[self.client_address] + if not self.websocket.closed: + self.websocket.close() + self.environ.update({ + 'wsgi.websocket': None + }) + self.websocket = None + + def run_application(self): + if (hasattr(self.server, 'pre_start_hook') and self.server.pre_start_hook): + self.logger.debug("Calling pre-start hook") + if self.server.pre_start_hook(self): + return super(WebSocketHandler, self).run_application() + + self.logger.debug("Initializing WebSocket") + self.result = self.upgrade_websocket() + + if hasattr(self, 'websocket'): + if self.status and not self.headers_sent: + self.write('') + + self.run_websocket() + else: + if self.status: + # A status was set, likely an error so just send the response + if not self.result: + self.result = [] + + self.process_result() + return + + # This handler did not handle the request, so defer it to the + # underlying application object + return super(WebSocketHandler, self).run_application() + + def upgrade_websocket(self): + """ + Attempt to upgrade the current environ into a websocket enabled + connection. If successful, the environ dict with be updated with two + new entries, `wsgi.websocket` and `wsgi.websocket_version`. + + :returns: Whether the upgrade was successful. + """ + + # Some basic sanity checks first + + self.logger.debug("Validating WebSocket request") + + if self.environ.get('REQUEST_METHOD', '') != 'GET': + # This is not a websocket request, so we must not handle it + self.logger.debug('Can only upgrade connection if using GET method.') + return + + upgrade = self.environ.get('HTTP_UPGRADE', '').lower() + + if upgrade == 'websocket': + connection = self.environ.get('HTTP_CONNECTION', '').lower() + + if 'upgrade' not in connection: + # This is not a websocket request, so we must not handle it + self.logger.warning("Client didn't ask for a connection " + "upgrade") + return + else: + # This is not a websocket request, so we must not handle it + return + + if self.request_version != 'HTTP/1.1': + self.start_response('402 Bad Request', []) + self.logger.warning("Bad server protocol in headers") + + return ['Bad protocol version'] + + if self.environ.get('HTTP_SEC_WEBSOCKET_VERSION'): + return self.upgrade_connection() + else: + self.logger.warning("No protocol defined") + self.start_response('426 Upgrade Required', [ + ('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))]) + + return ['No Websocket protocol version defined'] + + def upgrade_connection(self): + """ + Validate and 'upgrade' the HTTP request to a WebSocket request. + + If an upgrade succeeded then then handler will have `start_response` + with a status of `101`, the environ will also be updated with + `wsgi.websocket` and `wsgi.websocket_version` keys. + + :param environ: The WSGI environ dict. + :param start_response: The callable used to start the response. + :param stream: File like object that will be read from/written to by + the underlying WebSocket object, if created. + :return: The WSGI response iterator is something went awry. + """ + + self.logger.debug("Attempting to upgrade connection") + + version = self.environ.get("HTTP_SEC_WEBSOCKET_VERSION") + + if version not in self.SUPPORTED_VERSIONS: + msg = "Unsupported WebSocket Version: {0}".format(version) + + self.logger.warning(msg) + self.start_response('400 Bad Request', [ + ('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS)) + ]) + + return [msg] + + key = self.environ.get("HTTP_SEC_WEBSOCKET_KEY", '').strip() + + if not key: + # 5.2.1 (3) + msg = "Sec-WebSocket-Key header is missing/empty" + + self.logger.warning(msg) + self.start_response('400 Bad Request', []) + + return [msg] + + try: + key_len = len(base64.b64decode(key)) + except TypeError: + msg = "Invalid key: {0}".format(key) + + self.logger.warning(msg) + self.start_response('400 Bad Request', []) + + return [msg] + + if key_len != 16: + # 5.2.1 (3) + msg = "Invalid key: {0}".format(key) + + self.logger.warning(msg) + self.start_response('400 Bad Request', []) + + return [msg] + + # Check for WebSocket Protocols + requested_protocols = self.environ.get( + 'HTTP_SEC_WEBSOCKET_PROTOCOL', '') + protocol = None + + if hasattr(self.application, 'app_protocol'): + allowed_protocol = self.application.app_protocol( + self.environ['PATH_INFO']) + + if allowed_protocol and allowed_protocol in requested_protocols: + protocol = allowed_protocol + self.logger.debug("Protocol allowed: {0}".format(protocol)) + + self.websocket = WebSocket(self.environ, Stream(self), self) + self.environ.update({ + 'wsgi.websocket_version': version, + 'wsgi.websocket': self.websocket + }) + + if PY3: + accept = base64.b64encode( + hashlib.sha1((key + self.GUID).encode("latin-1")).digest() + ).decode("latin-1") + else: + accept = base64.b64encode(hashlib.sha1(key + self.GUID).digest()) + + headers = [ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Accept", accept) + ] + + if protocol: + headers.append(("Sec-WebSocket-Protocol", protocol)) + + self.logger.debug("WebSocket request accepted, switching protocols") + self.start_response("101 Switching Protocols", headers) + + @property + def logger(self): + if not hasattr(self.server, 'logger'): + self.server.logger = create_logger(__name__) + + return self.server.logger + + def log_request(self): + if '101' not in str(self.status): + self.logger.info(self.format_request()) + + @property + def active_client(self): + return self.server.clients[self.client_address] + + def start_response(self, status, headers, exc_info=None): + """ + Called when the handler is ready to send a response back to the remote + endpoint. A websocket connection may have not been created. + """ + writer = super(WebSocketHandler, self).start_response( + status, headers, exc_info=exc_info) + + self._prepare_response() + + return writer + + def _prepare_response(self): + """ + Sets up the ``pywsgi.Handler`` to work with a websocket response. + + This is used by other projects that need to support WebSocket + connections as part of a larger effort. + """ + assert not self.headers_sent + + if not self.environ.get('wsgi.websocket'): + # a WebSocket connection is not established, do nothing + return + + # So that `finalize_headers` doesn't write a Content-Length header + self.provided_content_length = False + + # The websocket is now controlling the response + self.response_use_chunked = False + + # Once the request is over, the connection must be closed + self.close_connection = True + + # Prevents the Date header from being written + self.provided_date = True diff --git a/libs/geventwebsocket/logging.py b/libs/geventwebsocket/logging.py new file mode 100644 index 000000000..554ca02d6 --- /dev/null +++ b/libs/geventwebsocket/logging.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import + +from logging import getLogger, StreamHandler, getLoggerClass, Formatter, DEBUG + + +def create_logger(name, debug=False, format=None): + Logger = getLoggerClass() + + class DebugLogger(Logger): + def getEffectiveLevel(x): + if x.level == 0 and debug: + return DEBUG + else: + return Logger.getEffectiveLevel(x) + + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if debug else None + + handler = DebugHandler() + handler.setLevel(DEBUG) + + if format: + handler.setFormatter(Formatter(format)) + + logger = getLogger(name) + del logger.handlers[:] + logger.__class__ = DebugLogger + logger.addHandler(handler) + + return logger diff --git a/libs/geventwebsocket/protocols/__init__.py b/libs/geventwebsocket/protocols/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/geventwebsocket/protocols/base.py b/libs/geventwebsocket/protocols/base.py new file mode 100644 index 000000000..1c05ab620 --- /dev/null +++ b/libs/geventwebsocket/protocols/base.py @@ -0,0 +1,35 @@ +class BaseProtocol(object): + PROTOCOL_NAME = '' + + def __init__(self, app): + self._app = app + + def on_open(self): + self.app.on_open() + + def on_message(self, message): + self.app.on_message(message) + + def on_close(self, reason=None): + self.app.on_close(reason) + + @property + def app(self): + if self._app: + return self._app + else: + raise Exception("No application coupled") + + @property + def server(self): + if not hasattr(self.app, 'ws'): + return None + + return self.app.ws.handler.server + + @property + def handler(self): + if not hasattr(self.app, 'ws'): + return None + + return self.app.ws.handler diff --git a/libs/geventwebsocket/protocols/wamp.py b/libs/geventwebsocket/protocols/wamp.py new file mode 100644 index 000000000..c89775be9 --- /dev/null +++ b/libs/geventwebsocket/protocols/wamp.py @@ -0,0 +1,235 @@ +import inspect +import random +import string +import types + +try: + import ujson as json +except ImportError: + try: + import simplejson as json + except ImportError: + import json + +from .._compat import range_type, string_types +from ..exceptions import WebSocketError +from .base import BaseProtocol + + +def export_rpc(arg=None): + if isinstance(arg, types.FunctionType): + arg._rpc = arg.__name__ + return arg + + +def serialize(data): + return json.dumps(data) + + +class Prefixes(object): + def __init__(self): + self.prefixes = {} + + def add(self, prefix, uri): + self.prefixes[prefix] = uri + + def resolve(self, curie_or_uri): + if "http://" in curie_or_uri: + return curie_or_uri + elif ':' in curie_or_uri: + prefix, proc = curie_or_uri.split(':', 1) + return self.prefixes[prefix] + proc + else: + raise Exception(curie_or_uri) + + +class RemoteProcedures(object): + def __init__(self): + self.calls = {} + + def register_procedure(self, uri, proc): + self.calls[uri] = proc + + def register_object(self, uri, obj): + for k in inspect.getmembers(obj, inspect.ismethod): + if '_rpc' in k[1].__dict__: + proc_uri = uri + k[1]._rpc + self.calls[proc_uri] = (obj, k[1]) + + def call(self, uri, args): + if uri in self.calls: + proc = self.calls[uri] + + # Do the correct call whether it's a function or instance method. + if isinstance(proc, tuple): + if proc[1].__self__ is None: + # Create instance of object and call method + return proc[1](proc[0](), *args) + else: + # Call bound method on instance + return proc[1](*args) + else: + return self.calls[uri](*args) + else: + raise Exception("no such uri '{}'".format(uri)) + + +class Channels(object): + def __init__(self): + self.channels = {} + + def create(self, uri, prefix_matching=False): + if uri not in self.channels: + self.channels[uri] = [] + + # TODO: implement prefix matching + + def subscribe(self, uri, client): + if uri in self.channels: + self.channels[uri].append(client) + + def unsubscribe(self, uri, client): + if uri not in self.channels: + return + + client_index = self.channels[uri].index(client) + self.channels[uri].pop(client_index) + + if len(self.channels[uri]) == 0: + del self.channels[uri] + + def publish(self, uri, event, exclude=None, eligible=None): + if uri not in self.channels: + return + + # TODO: exclude & eligible + + msg = [WampProtocol.MSG_EVENT, uri, event] + + for client in self.channels[uri]: + try: + client.ws.send(serialize(msg)) + except WebSocketError: + # Seems someone didn't unsubscribe before disconnecting + self.channels[uri].remove(client) + + +class WampProtocol(BaseProtocol): + MSG_WELCOME = 0 + MSG_PREFIX = 1 + MSG_CALL = 2 + MSG_CALL_RESULT = 3 + MSG_CALL_ERROR = 4 + MSG_SUBSCRIBE = 5 + MSG_UNSUBSCRIBE = 6 + MSG_PUBLISH = 7 + MSG_EVENT = 8 + + PROTOCOL_NAME = "wamp" + + def __init__(self, *args, **kwargs): + self.procedures = RemoteProcedures() + self.prefixes = Prefixes() + self.session_id = ''.join( + [random.choice(string.digits + string.letters) + for i in range_type(16)]) + + super(WampProtocol, self).__init__(*args, **kwargs) + + def register_procedure(self, *args, **kwargs): + self.procedures.register_procedure(*args, **kwargs) + + def register_object(self, *args, **kwargs): + self.procedures.register_object(*args, **kwargs) + + def register_pubsub(self, *args, **kwargs): + if not hasattr(self.server, 'channels'): + self.server.channels = Channels() + + self.server.channels.create(*args, **kwargs) + + def do_handshake(self): + from geventwebsocket import get_version + + welcome = [ + self.MSG_WELCOME, + self.session_id, + 1, + 'gevent-websocket/' + get_version() + ] + self.app.ws.send(serialize(welcome)) + + def _get_exception_info(self, e): + uri = 'http://TODO#generic' + desc = str(type(e)) + details = str(e) + return [uri, desc, details] + + def rpc_call(self, data): + call_id, curie_or_uri = data[1:3] + args = data[3:] + + if not isinstance(call_id, string_types): + raise Exception() + if not isinstance(curie_or_uri, string_types): + raise Exception() + + uri = self.prefixes.resolve(curie_or_uri) + + try: + result = self.procedures.call(uri, args) + result_msg = [self.MSG_CALL_RESULT, call_id, result] + except Exception as e: + result_msg = [self.MSG_CALL_ERROR, + call_id] + self._get_exception_info(e) + + self.app.on_message(serialize(result_msg)) + + def pubsub_action(self, data): + action = data[0] + curie_or_uri = data[1] + + if not isinstance(action, int): + raise Exception() + if not isinstance(curie_or_uri, string_types): + raise Exception() + + uri = self.prefixes.resolve(curie_or_uri) + + if action == self.MSG_SUBSCRIBE and len(data) == 2: + self.server.channels.subscribe(data[1], self.handler.active_client) + + elif action == self.MSG_UNSUBSCRIBE and len(data) == 2: + self.server.channels.unsubscribe( + data[1], self.handler.active_client) + + elif action == self.MSG_PUBLISH and len(data) >= 3: + payload = data[2] if len(data) >= 3 else None + exclude = data[3] if len(data) >= 4 else None + eligible = data[4] if len(data) >= 5 else None + + self.server.channels.publish(uri, payload, exclude, eligible) + + def on_open(self): + self.app.on_open() + self.do_handshake() + + def on_message(self, message): + data = json.loads(message) + + if not isinstance(data, list): + raise Exception('incoming data is no list') + + if data[0] == self.MSG_PREFIX and len(data) == 3: + prefix, uri = data[1:3] + self.prefixes.add(prefix, uri) + + elif data[0] == self.MSG_CALL and len(data) >= 3: + return self.rpc_call(data) + + elif data[0] in (self.MSG_SUBSCRIBE, self.MSG_UNSUBSCRIBE, + self.MSG_PUBLISH): + return self.pubsub_action(data) + else: + raise Exception("Unknown call") + diff --git a/libs/geventwebsocket/resource.py b/libs/geventwebsocket/resource.py new file mode 100644 index 000000000..549f0d32d --- /dev/null +++ b/libs/geventwebsocket/resource.py @@ -0,0 +1,100 @@ +import re +import warnings + +from .protocols.base import BaseProtocol +from .exceptions import WebSocketError + +try: + from collections import OrderedDict +except ImportError: + class OrderedDict: + pass + + +class WebSocketApplication(object): + protocol_class = BaseProtocol + + def __init__(self, ws): + self.protocol = self.protocol_class(self) + self.ws = ws + + def handle(self): + self.protocol.on_open() + + while True: + try: + message = self.ws.receive() + except WebSocketError: + self.protocol.on_close() + break + + self.protocol.on_message(message) + + def on_open(self, *args, **kwargs): + pass + + def on_close(self, *args, **kwargs): + pass + + def on_message(self, message, *args, **kwargs): + self.ws.send(message, **kwargs) + + @classmethod + def protocol_name(cls): + return cls.protocol_class.PROTOCOL_NAME + + +class Resource(object): + def __init__(self, apps=None): + self.apps = apps if apps else [] + + if isinstance(apps, dict): + if not isinstance(apps, OrderedDict): + warnings.warn("Using an unordered dictionary for the " + "app list is discouraged and may lead to " + "undefined behavior.", UserWarning) + + self.apps = apps.items() + + # An app can either be a standard WSGI application (an object we call with + # __call__(self, environ, start_response)) or a class we instantiate + # (and which can handle websockets). This function tells them apart. + # Override this if you have apps that can handle websockets but don't + # fulfill these criteria. + def _is_websocket_app(self, app): + return isinstance(app, type) and issubclass(app, WebSocketApplication) + + def _app_by_path(self, environ_path, is_websocket_request): + # Which app matched the current path? + for path, app in self.apps: + if re.match(path, environ_path): + if is_websocket_request == self._is_websocket_app(app): + return app + return None + + def app_protocol(self, path): + # app_protocol will only be called for websocket apps + app = self._app_by_path(path, True) + + if hasattr(app, 'protocol_name'): + return app.protocol_name() + else: + return '' + + def __call__(self, environ, start_response): + environ = environ + is_websocket_call = 'wsgi.websocket' in environ + current_app = self._app_by_path(environ['PATH_INFO'], is_websocket_call) + + if current_app is None: + raise Exception("No apps defined") + + if is_websocket_call: + ws = environ['wsgi.websocket'] + current_app = current_app(ws) + current_app.ws = ws # TODO: needed? + current_app.handle() + # Always return something, calling WSGI middleware may rely on it + return [] + else: + return current_app(environ, start_response) diff --git a/libs/geventwebsocket/server.py b/libs/geventwebsocket/server.py new file mode 100644 index 000000000..e939bd118 --- /dev/null +++ b/libs/geventwebsocket/server.py @@ -0,0 +1,34 @@ +from gevent.pywsgi import WSGIServer + +from .handler import WebSocketHandler +from .logging import create_logger + + +class WebSocketServer(WSGIServer): + handler_class = WebSocketHandler + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + + def __init__(self, *args, **kwargs): + self.debug = kwargs.pop('debug', False) + self.pre_start_hook = kwargs.pop('pre_start_hook', None) + self._logger = None + self.clients = {} + + super(WebSocketServer, self).__init__(*args, **kwargs) + + def handle(self, socket, address): + handler = self.handler_class(socket, address, self) + handler.handle() + + @property + def logger(self): + if not self._logger: + self._logger = create_logger( + __name__, self.debug, self.debug_log_format) + + return self._logger diff --git a/libs/geventwebsocket/utf8validator.py b/libs/geventwebsocket/utf8validator.py new file mode 100644 index 000000000..d604f9663 --- /dev/null +++ b/libs/geventwebsocket/utf8validator.py @@ -0,0 +1,224 @@ +from ._compat import PY3 + +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### + +# Note: This code is a Python implementation of the algorithm +# "Flexible and Economical UTF-8 Decoder" by Bjoern Hoehrmann +# bjoern@hoehrmann.de, http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + +__all__ = ("Utf8Validator",) + + +# DFA transitions +UTF8VALIDATOR_DFA = ( + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 00..1f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 20..3f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 40..5f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 60..7f + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, # 80..9f + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, # a0..bf + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # c0..df + 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, # e0..ef + 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, # f0..ff + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, # s0..s0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, # s1..s2 + 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, # s3..s4 + 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, # s5..s6 + 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # s7..s8 +) + +UTF8_ACCEPT = 0 +UTF8_REJECT = 1 + + +# use Cython implementation of UTF8 validator if available +# +try: + from wsaccel.utf8validator import Utf8Validator + +except ImportError: + # + # Fallback to pure Python implementation - also for PyPy. + # + # Do NOT touch this code unless you know what you are doing! + # https://github.com/oberstet/scratchbox/tree/master/python/utf8 + # + + if PY3: + + # Python 3 and above + + # convert DFA table to bytes (performance) + UTF8VALIDATOR_DFA_S = bytes(UTF8VALIDATOR_DFA) + + class Utf8Validator(object): + """ + Incremental UTF-8 validator with constant memory consumption (minimal state). + + Implements the algorithm "Flexible and Economical UTF-8 Decoder" by + Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/). + """ + + def __init__(self): + self.reset() + + def decode(self, b): + """ + Eat one UTF-8 octet, and validate on the fly. + + Returns ``UTF8_ACCEPT`` when enough octets have been consumed, in which case + ``self.codepoint`` contains the decoded Unicode code point. + + Returns ``UTF8_REJECT`` when invalid UTF-8 was encountered. + + Returns some other positive integer when more octets need to be eaten. + """ + tt = UTF8VALIDATOR_DFA_S[b] + if self.state != UTF8_ACCEPT: + self.codepoint = (b & 0x3f) | (self.codepoint << 6) + else: + self.codepoint = (0xff >> tt) & b + self.state = UTF8VALIDATOR_DFA_S[256 + self.state * 16 + tt] + return self.state + + def reset(self): + """ + Reset validator to start new incremental UTF-8 decode/validation. + """ + self.state = UTF8_ACCEPT # the empty string is valid UTF8 + self.codepoint = 0 + self.i = 0 + + def validate(self, ba): + """ + Incrementally validate a chunk of bytes provided as string. + + Will return a quad ``(valid?, endsOnCodePoint?, currentIndex, totalIndex)``. + + As soon as an octet is encountered which renders the octet sequence + invalid, a quad with ``valid? == False`` is returned. ``currentIndex`` returns + the index within the currently consumed chunk, and ``totalIndex`` the + index within the total consumed sequence that was the point of bail out. + When ``valid? == True``, currentIndex will be ``len(ba)`` and ``totalIndex`` the + total amount of consumed bytes. + """ + # + # The code here is written for optimal JITting in PyPy, not for best + # readability by your grandma or particular elegance. Do NOT touch! + # + l = len(ba) + i = 0 + state = self.state + while i < l: + # optimized version of decode(), since we are not interested in actual code points + state = UTF8VALIDATOR_DFA_S[256 + (state << 4) + UTF8VALIDATOR_DFA_S[ba[i]]] + if state == UTF8_REJECT: + self.state = state + self.i += i + return False, False, i, self.i + i += 1 + self.state = state + self.i += l + return True, state == UTF8_ACCEPT, l, self.i + + else: + + # convert DFA table to string (performance) + UTF8VALIDATOR_DFA_S = ''.join([chr(c) for c in UTF8VALIDATOR_DFA]) + + class Utf8Validator(object): + """ + Incremental UTF-8 validator with constant memory consumption (minimal state). + + Implements the algorithm "Flexible and Economical UTF-8 Decoder" by + Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/). + """ + + def __init__(self): + self.reset() + + def decode(self, b): + """ + Eat one UTF-8 octet, and validate on the fly. + + Returns ``UTF8_ACCEPT`` when enough octets have been consumed, in which case + ``self.codepoint`` contains the decoded Unicode code point. + + Returns ``UTF8_REJECT`` when invalid UTF-8 was encountered. + + Returns some other positive integer when more octets need to be eaten. + """ + tt = ord(UTF8VALIDATOR_DFA_S[b]) + if self.state != UTF8_ACCEPT: + self.codepoint = (b & 0x3f) | (self.codepoint << 6) + else: + self.codepoint = (0xff >> tt) & b + self.state = ord(UTF8VALIDATOR_DFA_S[256 + self.state * 16 + tt]) + return self.state + + def reset(self): + """ + Reset validator to start new incremental UTF-8 decode/validation. + """ + self.state = UTF8_ACCEPT # the empty string is valid UTF8 + self.codepoint = 0 + self.i = 0 + + def validate(self, ba): + """ + Incrementally validate a chunk of bytes provided as string. + + Will return a quad ``(valid?, endsOnCodePoint?, currentIndex, totalIndex)``. + + As soon as an octet is encountered which renders the octet sequence + invalid, a quad with ``valid? == False`` is returned. ``currentIndex`` returns + the index within the currently consumed chunk, and ``totalIndex`` the + index within the total consumed sequence that was the point of bail out. + When ``valid? == True``, currentIndex will be ``len(ba)`` and ``totalIndex`` the + total amount of consumed bytes. + """ + # + # The code here is written for optimal JITting in PyPy, not for best + # readability by your grandma or particular elegance. Do NOT touch! + # + l = len(ba) + i = 0 + state = self.state + while i < l: + # optimized version of decode(), since we are not interested in actual code points + try: + state = ord(UTF8VALIDATOR_DFA_S[256 + (state << 4) + ord(UTF8VALIDATOR_DFA_S[ba[i]])]) + except: + import ipdb; ipdb.set_trace() + if state == UTF8_REJECT: + self.state = state + self.i += i + return False, False, i, self.i + i += 1 + self.state = state + self.i += l + return True, state == UTF8_ACCEPT, l, self.i diff --git a/libs/geventwebsocket/utils.py b/libs/geventwebsocket/utils.py new file mode 100644 index 000000000..2e5bc3b7e --- /dev/null +++ b/libs/geventwebsocket/utils.py @@ -0,0 +1,45 @@ +import subprocess + + +def get_version(version=None): + "Returns a PEP 386-compliant version number from VERSION." + + if version is None: + from geventwebsocket import VERSION as version + else: + assert len(version) == 5 + assert version[3] in ('alpha', 'beta', 'rc', 'final') + + # Now build the two parts of the version number: + # main = X.Y[.Z] + # sub = .devN - for pre-alpha releases + # | {a|b|c}N - for alpha, beta and rc releases + + parts = 2 if version[2] == 0 else 3 + main = '.'.join(str(x) for x in version[:parts]) + + sub = '' + if version[3] == 'alpha' and version[4] == 0: + hg_changeset = get_hg_changeset() + if hg_changeset: + sub = '.dev{0}'.format(hg_changeset) + + elif version[3] != 'final': + mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} + sub = mapping[version[3]] + str(version[4]) + + return str(main + sub) + + +def get_hg_changeset(): + rev, err = subprocess.Popen( + 'hg id -i', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ).communicate() + + if err: + return None + else: + return rev.strip().replace('+', '') diff --git a/libs/geventwebsocket/websocket.py b/libs/geventwebsocket/websocket.py new file mode 100644 index 000000000..45579261e --- /dev/null +++ b/libs/geventwebsocket/websocket.py @@ -0,0 +1,565 @@ +import struct + +from socket import error + +from ._compat import string_types, range_type, text_type +from .exceptions import ProtocolError +from .exceptions import WebSocketError +from .exceptions import FrameTooLargeException +from .utf8validator import Utf8Validator + + +MSG_SOCKET_DEAD = "Socket is dead" +MSG_ALREADY_CLOSED = "Connection is already closed" +MSG_CLOSED = "Connection closed" + + +class WebSocket(object): + """ + Base class for supporting websocket operations. + + :ivar environ: The http environment referenced by this connection. + :ivar closed: Whether this connection is closed/closing. + :ivar stream: The underlying file like object that will be read from / + written to by this WebSocket object. + """ + + __slots__ = ('utf8validator', 'utf8validate_last', 'environ', 'closed', + 'stream', 'raw_write', 'raw_read', 'handler') + + OPCODE_CONTINUATION = 0x00 + OPCODE_TEXT = 0x01 + OPCODE_BINARY = 0x02 + OPCODE_CLOSE = 0x08 + OPCODE_PING = 0x09 + OPCODE_PONG = 0x0a + + def __init__(self, environ, stream, handler): + self.environ = environ + self.closed = False + + self.stream = stream + + self.raw_write = stream.write + self.raw_read = stream.read + + self.utf8validator = Utf8Validator() + self.handler = handler + + def __del__(self): + try: + self.close() + except: + # close() may fail if __init__ didn't complete + pass + + def _decode_bytes(self, bytestring): + """ + Internal method used to convert the utf-8 encoded bytestring into + unicode. + + If the conversion fails, the socket will be closed. + """ + + if not bytestring: + return '' + + try: + return bytestring.decode('utf-8') + except UnicodeDecodeError: + self.close(1007) + + raise + + def _encode_bytes(self, text): + """ + :returns: The utf-8 byte string equivalent of `text`. + """ + + if not isinstance(text, str): + text = text_type(text or '') + + return text.encode("utf-8") + + def _is_valid_close_code(self, code): + """ + :returns: Whether the returned close code is a valid hybi return code. + """ + if code < 1000: + return False + + if 1004 <= code <= 1006: + return False + + if 1012 <= code <= 1016: + return False + + if code == 1100: + # not sure about this one but the autobahn fuzzer requires it. + return False + + if 2000 <= code <= 2999: + return False + + return True + + @property + def current_app(self): + if hasattr(self.handler.server.application, 'current_app'): + return self.handler.server.application.current_app + else: + # For backwards compatibility reasons + class MockApp(): + def on_close(self, *args): + pass + + return MockApp() + + @property + def origin(self): + if not self.environ: + return + + return self.environ.get('HTTP_ORIGIN') + + @property + def protocol(self): + if not self.environ: + return + + return self.environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') + + @property + def version(self): + if not self.environ: + return + + return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION') + + @property + def path(self): + if not self.environ: + return + + return self.environ.get('PATH_INFO') + + @property + def logger(self): + return self.handler.logger + + def handle_close(self, header, payload): + """ + Called when a close frame has been decoded from the stream. + + :param header: The decoded `Header`. + :param payload: The bytestring payload associated with the close frame. + """ + if not payload: + self.close(1000, None) + + return + + if len(payload) < 2: + raise ProtocolError('Invalid close frame: {0} {1}'.format( + header, payload)) + + code = struct.unpack('!H', payload[:2])[0] + payload = payload[2:] + + if payload: + validator = Utf8Validator() + val = validator.validate(payload) + + if not val[0]: + raise UnicodeError + + if not self._is_valid_close_code(code): + raise ProtocolError('Invalid close code {0}'.format(code)) + + self.close(code, payload) + + def handle_ping(self, header, payload): + self.send_frame(payload, self.OPCODE_PONG) + + def handle_pong(self, header, payload): + pass + + def read_frame(self): + """ + Block until a full frame has been read from the socket. + + This is an internal method as calling this will not cleanup correctly + if an exception is called. Use `receive` instead. + + :return: The header and payload as a tuple. + """ + + header = Header.decode_header(self.stream) + + if header.flags: + raise ProtocolError + + if not header.length: + return header, b'' + + try: + payload = self.raw_read(header.length) + except error: + payload = b'' + except Exception: + # TODO log out this exception + payload = b'' + + if len(payload) != header.length: + raise WebSocketError('Unexpected EOF reading frame payload') + + if header.mask: + payload = header.unmask_payload(payload) + + return header, payload + + def validate_utf8(self, payload): + # Make sure the frames are decodable independently + self.utf8validate_last = self.utf8validator.validate(payload) + + if not self.utf8validate_last[0]: + raise UnicodeError("Encountered invalid UTF-8 while processing " + "text message at payload octet index " + "{0:d}".format(self.utf8validate_last[3])) + + def read_message(self): + """ + Return the next text or binary message from the socket. + + This is an internal method as calling this will not cleanup correctly + if an exception is called. Use `receive` instead. + """ + opcode = None + message = bytearray() + + while True: + header, payload = self.read_frame() + f_opcode = header.opcode + + if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY): + # a new frame + if opcode: + raise ProtocolError("The opcode in non-fin frame is " + "expected to be zero, got " + "{0!r}".format(f_opcode)) + + # Start reading a new message, reset the validator + self.utf8validator.reset() + self.utf8validate_last = (True, True, 0, 0) + + opcode = f_opcode + + elif f_opcode == self.OPCODE_CONTINUATION: + if not opcode: + raise ProtocolError("Unexpected frame with opcode=0") + + elif f_opcode == self.OPCODE_PING: + self.handle_ping(header, payload) + continue + + elif f_opcode == self.OPCODE_PONG: + self.handle_pong(header, payload) + continue + + elif f_opcode == self.OPCODE_CLOSE: + self.handle_close(header, payload) + return + + else: + raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode)) + + if opcode == self.OPCODE_TEXT: + self.validate_utf8(payload) + + message += payload + + if header.fin: + break + + if opcode == self.OPCODE_TEXT: + self.validate_utf8(message) + return self._decode_bytes(message) + else: + return message + + def receive(self): + """ + Read and return a message from the stream. If `None` is returned, then + the socket is considered closed/errored. + """ + + if self.closed: + self.current_app.on_close(MSG_ALREADY_CLOSED) + raise WebSocketError(MSG_ALREADY_CLOSED) + + try: + return self.read_message() + except UnicodeError: + self.close(1007) + except ProtocolError: + self.close(1002) + except error: + self.close() + self.current_app.on_close(MSG_CLOSED) + + return None + + def send_frame(self, message, opcode): + """ + Send a frame over the websocket with message as its payload + """ + if self.closed: + self.current_app.on_close(MSG_ALREADY_CLOSED) + raise WebSocketError(MSG_ALREADY_CLOSED) + + if opcode in (self.OPCODE_TEXT, self.OPCODE_PING): + message = self._encode_bytes(message) + elif opcode == self.OPCODE_BINARY: + message = bytes(message) + + header = Header.encode_header(True, opcode, b'', len(message), 0) + + try: + self.raw_write(header + message) + except error: + raise WebSocketError(MSG_SOCKET_DEAD) + except: + raise + + def send(self, message, binary=None): + """ + Send a frame over the websocket with message as its payload + """ + if binary is None: + binary = not isinstance(message, string_types) + + opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT + + try: + self.send_frame(message, opcode) + except WebSocketError: + self.current_app.on_close(MSG_SOCKET_DEAD) + raise WebSocketError(MSG_SOCKET_DEAD) + + def close(self, code=1000, message=b''): + """ + Close the websocket and connection, sending the specified code and + message. The underlying socket object is _not_ closed, that is the + responsibility of the initiator. + """ + + if self.closed: + self.current_app.on_close(MSG_ALREADY_CLOSED) + + try: + message = self._encode_bytes(message) + + self.send_frame(message, opcode=self.OPCODE_CLOSE) + except WebSocketError: + # Failed to write the closing frame but it's ok because we're + # closing the socket anyway. + self.logger.debug("Failed to write closing frame -> closing socket") + finally: + self.logger.debug("Closed WebSocket") + self.closed = True + + self.stream = None + self.raw_write = None + self.raw_read = None + + self.environ = None + + #self.current_app.on_close(MSG_ALREADY_CLOSED) + + +class Stream(object): + """ + Wraps the handler's socket/rfile attributes and makes it in to a file like + object that can be read from/written to by the lower level websocket api. + """ + + __slots__ = ('handler', 'read', 'write') + + def __init__(self, handler): + self.handler = handler + self.read = handler.rfile.read + self.write = handler.socket.sendall + + +class Header(object): + __slots__ = ('fin', 'mask', 'opcode', 'flags', 'length') + + FIN_MASK = 0x80 + OPCODE_MASK = 0x0f + MASK_MASK = 0x80 + LENGTH_MASK = 0x7f + + RSV0_MASK = 0x40 + RSV1_MASK = 0x20 + RSV2_MASK = 0x10 + + # bitwise mask that will determine the reserved bits for a frame header + HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK + + def __init__(self, fin=0, opcode=0, flags=0, length=0): + self.mask = '' + self.fin = fin + self.opcode = opcode + self.flags = flags + self.length = length + + def mask_payload(self, payload): + payload = bytearray(payload) + mask = bytearray(self.mask) + + for i in range_type(self.length): + payload[i] ^= mask[i % 4] + + return payload + + # it's the same operation + unmask_payload = mask_payload + + def __repr__(self): + opcodes = { + 0: 'continuation(0)', + 1: 'text(1)', + 2: 'binary(2)', + 8: 'close(8)', + 9: 'ping(9)', + 10: 'pong(10)' + } + flags = { + 0x40: 'RSV1 MASK', + 0x20: 'RSV2 MASK', + 0x10: 'RSV3 MASK' + } + + return ("
").format( + self.fin, + opcodes.get(self.opcode, 'reserved({})'.format(self.opcode)), + self.length, + flags.get(self.flags, 'reserved({})'.format(self.flags)), + self.mask, id(self) + ) + + @classmethod + def decode_header(cls, stream): + """ + Decode a WebSocket header. + + :param stream: A file like object that can be 'read' from. + :returns: A `Header` instance. + """ + read = stream.read + data = read(2) + + if len(data) != 2: + raise WebSocketError("Unexpected EOF while decoding header") + + first_byte, second_byte = struct.unpack('!BB', data) + + header = cls( + fin=first_byte & cls.FIN_MASK == cls.FIN_MASK, + opcode=first_byte & cls.OPCODE_MASK, + flags=first_byte & cls.HEADER_FLAG_MASK, + length=second_byte & cls.LENGTH_MASK) + + has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK + + if header.opcode > 0x07: + if not header.fin: + raise ProtocolError( + "Received fragmented control frame: {0!r}".format(data)) + + # Control frames MUST have a payload length of 125 bytes or less + if header.length > 125: + raise FrameTooLargeException( + "Control frame cannot be larger than 125 bytes: " + "{0!r}".format(data)) + + if header.length == 126: + # 16 bit length + data = read(2) + + if len(data) != 2: + raise WebSocketError('Unexpected EOF while decoding header') + + header.length = struct.unpack('!H', data)[0] + elif header.length == 127: + # 64 bit length + data = read(8) + + if len(data) != 8: + raise WebSocketError('Unexpected EOF while decoding header') + + header.length = struct.unpack('!Q', data)[0] + + if has_mask: + mask = read(4) + + if len(mask) != 4: + raise WebSocketError('Unexpected EOF while decoding header') + + header.mask = mask + + return header + + @classmethod + def encode_header(cls, fin, opcode, mask, length, flags): + """ + Encodes a WebSocket header. + + :param fin: Whether this is the final frame for this opcode. + :param opcode: The opcode of the payload, see `OPCODE_*` + :param mask: Whether the payload is masked. + :param length: The length of the frame. + :param flags: The RSV* flags. + :return: A bytestring encoded header. + """ + first_byte = opcode + second_byte = 0 + extra = b"" + result = bytearray() + + if fin: + first_byte |= cls.FIN_MASK + + if flags & cls.RSV0_MASK: + first_byte |= cls.RSV0_MASK + + if flags & cls.RSV1_MASK: + first_byte |= cls.RSV1_MASK + + if flags & cls.RSV2_MASK: + first_byte |= cls.RSV2_MASK + + # now deal with length complexities + if length < 126: + second_byte += length + elif length <= 0xffff: + second_byte += 126 + extra = struct.pack('!H', length) + elif length <= 0xffffffffffffffff: + second_byte += 127 + extra = struct.pack('!Q', length) + else: + raise FrameTooLargeException + + if mask: + second_byte |= cls.MASK_MASK + + result.append(first_byte) + result.append(second_byte) + result.extend(extra) + + if mask: + result.extend(mask) + + return result diff --git a/libs/html5lib/__init__.py b/libs/html5lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/version.txt b/libs/version.txt index a068ef849..02e455f30 100644 --- a/libs/version.txt +++ b/libs/version.txt @@ -9,6 +9,7 @@ chardet=3.0.4 configparser2=4.0.0 dogpile.cache=0.6.5 enzyme=0.4.1 +geventwebsocker=0.10.1 gitpython=2.1.9 guessit=2.1.4 langdetect=1.0.7 diff --git a/static/noty/noty.css b/static/noty/noty.css new file mode 100644 index 000000000..1c3ce94b9 --- /dev/null +++ b/static/noty/noty.css @@ -0,0 +1,697 @@ +.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight { + position: fixed; + margin: 0; + padding: 0; + z-index: 9999999; + -webkit-transform: translateZ(0) scale(1, 1); + transform: translateZ(0) scale(1, 1); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-font-smoothing: subpixel-antialiased; + filter: blur(0); + -webkit-filter: blur(0); + max-width: 90%; } + +#noty_layout__top { + top: 0; + left: 5%; + width: 90%; } + +#noty_layout__topLeft { + top: 20px; + left: 20px; + width: 325px; } + +#noty_layout__topCenter { + top: 5%; + left: 50%; + width: 325px; + -webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__topRight { + top: 20px; + right: 20px; + width: 325px; } + +#noty_layout__bottom { + bottom: 0; + left: 5%; + width: 90%; } + +#noty_layout__bottomLeft { + bottom: 20px; + left: 20px; + width: 325px; } + +#noty_layout__bottomCenter { + bottom: 5%; + left: 50%; + width: 325px; + -webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__bottomRight { + bottom: 20px; + right: 20px; + width: 325px; } + +#noty_layout__center { + top: 50%; + left: 50%; + width: 325px; + -webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__centerLeft { + top: 50%; + left: 20px; + width: 325px; + -webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__centerRight { + top: 50%; + right: 20px; + width: 325px; + -webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +.noty_progressbar { + display: none; } + +.noty_has_timeout.noty_has_progressbar .noty_progressbar { + display: block; + position: absolute; + left: 0; + bottom: 0; + height: 3px; + width: 100%; + background-color: #646464; + opacity: 0.2; + filter: alpha(opacity=10); } + +.noty_bar { + -webkit-backface-visibility: hidden; + -webkit-transform: translate(0, 0) translateZ(0) scale(1, 1); + -ms-transform: translate(0, 0) scale(1, 1); + transform: translate(0, 0) scale(1, 1); + -webkit-font-smoothing: subpixel-antialiased; + overflow: hidden; } + +.noty_effects_open { + opacity: 0; + -webkit-transform: translate(50%); + -ms-transform: translate(50%); + transform: translate(50%); + -webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } + +.noty_effects_close { + -webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } + +.noty_fix_effects_height { + -webkit-animation: noty_anim_height 75ms ease-out; + animation: noty_anim_height 75ms ease-out; } + +.noty_close_with_click { + cursor: pointer; } + +.noty_close_button { + position: absolute; + top: 2px; + right: 2px; + font-weight: bold; + width: 20px; + height: 20px; + text-align: center; + line-height: 20px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 2px; + cursor: pointer; + -webkit-transition: all .2s ease-out; + transition: all .2s ease-out; } + +.noty_close_button:hover { + background-color: rgba(0, 0, 0, 0.1); } + +.noty_modal { + position: fixed; + width: 100%; + height: 100%; + background-color: #000; + z-index: 10000; + opacity: .3; + left: 0; + top: 0; } + +.noty_modal.noty_modal_open { + opacity: 0; + -webkit-animation: noty_modal_in .3s ease-out; + animation: noty_modal_in .3s ease-out; } + +.noty_modal.noty_modal_close { + -webkit-animation: noty_modal_out .3s ease-out; + animation: noty_modal_out .3s ease-out; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } + +@-webkit-keyframes noty_modal_in { + 100% { + opacity: .3; } } + +@keyframes noty_modal_in { + 100% { + opacity: .3; } } + +@-webkit-keyframes noty_modal_out { + 100% { + opacity: 0; } } + +@keyframes noty_modal_out { + 100% { + opacity: 0; } } + +@keyframes noty_modal_out { + 100% { + opacity: 0; } } + +@-webkit-keyframes noty_anim_in { + 100% { + -webkit-transform: translate(0); + transform: translate(0); + opacity: 1; } } + +@keyframes noty_anim_in { + 100% { + -webkit-transform: translate(0); + transform: translate(0); + opacity: 1; } } + +@-webkit-keyframes noty_anim_out { + 100% { + -webkit-transform: translate(50%); + transform: translate(50%); + opacity: 0; } } + +@keyframes noty_anim_out { + 100% { + -webkit-transform: translate(50%); + transform: translate(50%); + opacity: 0; } } + +@-webkit-keyframes noty_anim_height { + 100% { + height: 0; } } + +@keyframes noty_anim_height { + 100% { + height: 0; } } + +.noty_theme__relax.noty_bar { + margin: 4px 0; + overflow: hidden; + border-radius: 2px; + position: relative; } + .noty_theme__relax.noty_bar .noty_body { + padding: 10px; } + .noty_theme__relax.noty_bar .noty_buttons { + border-top: 1px solid #e7e7e7; + padding: 5px 10px; } + +.noty_theme__relax.noty_type__alert, +.noty_theme__relax.noty_type__notification { + background-color: #fff; + border: 1px solid #dedede; + color: #444; } + +.noty_theme__relax.noty_type__warning { + background-color: #FFEAA8; + border: 1px solid #FFC237; + color: #826200; } + .noty_theme__relax.noty_type__warning .noty_buttons { + border-color: #dfaa30; } + +.noty_theme__relax.noty_type__error { + background-color: #FF8181; + border: 1px solid #e25353; + color: #FFF; } + .noty_theme__relax.noty_type__error .noty_buttons { + border-color: darkred; } + +.noty_theme__relax.noty_type__info, +.noty_theme__relax.noty_type__information { + background-color: #78C5E7; + border: 1px solid #3badd6; + color: #FFF; } + .noty_theme__relax.noty_type__info .noty_buttons, + .noty_theme__relax.noty_type__information .noty_buttons { + border-color: #0B90C4; } + +.noty_theme__relax.noty_type__success { + background-color: #BCF5BC; + border: 1px solid #7cdd77; + color: darkgreen; } + .noty_theme__relax.noty_type__success .noty_buttons { + border-color: #50C24E; } + +.noty_theme__metroui.noty_bar { + margin: 4px 0; + overflow: hidden; + position: relative; + box-shadow: rgba(0, 0, 0, 0.298039) 0 0 5px 0; } + .noty_theme__metroui.noty_bar .noty_progressbar { + position: absolute; + left: 0; + bottom: 0; + height: 3px; + width: 100%; + background-color: #000; + opacity: 0.2; + filter: alpha(opacity=20); } + .noty_theme__metroui.noty_bar .noty_body { + padding: 1.25em; + font-size: 14px; } + .noty_theme__metroui.noty_bar .noty_buttons { + padding: 0 10px .5em 10px; } + +.noty_theme__metroui.noty_type__alert, +.noty_theme__metroui.noty_type__notification { + background-color: #fff; + color: #1d1d1d; } + +.noty_theme__metroui.noty_type__warning { + background-color: #FA6800; + color: #fff; } + +.noty_theme__metroui.noty_type__error { + background-color: #CE352C; + color: #FFF; } + +.noty_theme__metroui.noty_type__info, +.noty_theme__metroui.noty_type__information { + background-color: #1BA1E2; + color: #FFF; } + +.noty_theme__metroui.noty_type__success { + background-color: #60A917; + color: #fff; } + +.noty_theme__mint.noty_bar { + margin: 4px 0; + overflow: hidden; + border-radius: 2px; + position: relative; } + .noty_theme__mint.noty_bar .noty_body { + padding: 10px; + font-size: 14px; } + .noty_theme__mint.noty_bar .noty_buttons { + padding: 10px; } + +.noty_theme__mint.noty_type__alert, +.noty_theme__mint.noty_type__notification { + background-color: #fff; + border-bottom: 1px solid #D1D1D1; + color: #2F2F2F; } + +.noty_theme__mint.noty_type__warning { + background-color: #FFAE42; + border-bottom: 1px solid #E89F3C; + color: #fff; } + +.noty_theme__mint.noty_type__error { + background-color: #DE636F; + border-bottom: 1px solid #CA5A65; + color: #fff; } + +.noty_theme__mint.noty_type__info, +.noty_theme__mint.noty_type__information { + background-color: #7F7EFF; + border-bottom: 1px solid #7473E8; + color: #fff; } + +.noty_theme__mint.noty_type__success { + background-color: #AFC765; + border-bottom: 1px solid #A0B55C; + color: #fff; } + +.noty_theme__sunset.noty_bar { + margin: 4px 0; + overflow: hidden; + border-radius: 2px; + position: relative; } + .noty_theme__sunset.noty_bar .noty_body { + padding: 10px; + font-size: 14px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); } + .noty_theme__sunset.noty_bar .noty_buttons { + padding: 10px; } + +.noty_theme__sunset.noty_type__alert, +.noty_theme__sunset.noty_type__notification { + background-color: #073B4C; + color: #fff; } + .noty_theme__sunset.noty_type__alert .noty_progressbar, + .noty_theme__sunset.noty_type__notification .noty_progressbar { + background-color: #fff; } + +.noty_theme__sunset.noty_type__warning { + background-color: #FFD166; + color: #fff; } + +.noty_theme__sunset.noty_type__error { + background-color: #EF476F; + color: #fff; } + .noty_theme__sunset.noty_type__error .noty_progressbar { + opacity: .4; } + +.noty_theme__sunset.noty_type__info, +.noty_theme__sunset.noty_type__information { + background-color: #118AB2; + color: #fff; } + .noty_theme__sunset.noty_type__info .noty_progressbar, + .noty_theme__sunset.noty_type__information .noty_progressbar { + opacity: .6; } + +.noty_theme__sunset.noty_type__success { + background-color: #06D6A0; + color: #fff; } + +.noty_theme__bootstrap-v3.noty_bar { + margin: 4px 0; + overflow: hidden; + position: relative; + border: 1px solid transparent; + border-radius: 4px; } + .noty_theme__bootstrap-v3.noty_bar .noty_body { + padding: 15px; } + .noty_theme__bootstrap-v3.noty_bar .noty_buttons { + padding: 10px; } + .noty_theme__bootstrap-v3.noty_bar .noty_close_button { + font-size: 21px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; + background: transparent; } + .noty_theme__bootstrap-v3.noty_bar .noty_close_button:hover { + background: transparent; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; } + +.noty_theme__bootstrap-v3.noty_type__alert, +.noty_theme__bootstrap-v3.noty_type__notification { + background-color: #fff; + color: inherit; } + +.noty_theme__bootstrap-v3.noty_type__warning { + background-color: #fcf8e3; + color: #8a6d3b; + border-color: #faebcc; } + +.noty_theme__bootstrap-v3.noty_type__error { + background-color: #f2dede; + color: #a94442; + border-color: #ebccd1; } + +.noty_theme__bootstrap-v3.noty_type__info, +.noty_theme__bootstrap-v3.noty_type__information { + background-color: #d9edf7; + color: #31708f; + border-color: #bce8f1; } + +.noty_theme__bootstrap-v3.noty_type__success { + background-color: #dff0d8; + color: #3c763d; + border-color: #d6e9c6; } + +.noty_theme__bootstrap-v4.noty_bar { + margin: 4px 0; + overflow: hidden; + position: relative; + border: 1px solid transparent; + border-radius: .25rem; } + .noty_theme__bootstrap-v4.noty_bar .noty_body { + padding: .75rem 1.25rem; } + .noty_theme__bootstrap-v4.noty_bar .noty_buttons { + padding: 10px; } + .noty_theme__bootstrap-v4.noty_bar .noty_close_button { + font-size: 1.5rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .5; + background: transparent; } + .noty_theme__bootstrap-v4.noty_bar .noty_close_button:hover { + background: transparent; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .75; } + +.noty_theme__bootstrap-v4.noty_type__alert, +.noty_theme__bootstrap-v4.noty_type__notification { + background-color: #fff; + color: inherit; } + +.noty_theme__bootstrap-v4.noty_type__warning { + background-color: #fcf8e3; + color: #8a6d3b; + border-color: #faebcc; } + +.noty_theme__bootstrap-v4.noty_type__error { + background-color: #f2dede; + color: #a94442; + border-color: #ebccd1; } + +.noty_theme__bootstrap-v4.noty_type__info, +.noty_theme__bootstrap-v4.noty_type__information { + background-color: #d9edf7; + color: #31708f; + border-color: #bce8f1; } + +.noty_theme__bootstrap-v4.noty_type__success { + background-color: #dff0d8; + color: #3c763d; + border-color: #d6e9c6; } + +.noty_theme__semanticui.noty_bar { + margin: 4px 0; + overflow: hidden; + position: relative; + border: 1px solid transparent; + font-size: 1em; + border-radius: .28571429rem; + box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 0 0 0 transparent; } + .noty_theme__semanticui.noty_bar .noty_body { + padding: 1em 1.5em; + line-height: 1.4285em; } + .noty_theme__semanticui.noty_bar .noty_buttons { + padding: 10px; } + +.noty_theme__semanticui.noty_type__alert, +.noty_theme__semanticui.noty_type__notification { + background-color: #f8f8f9; + color: rgba(0, 0, 0, 0.87); } + +.noty_theme__semanticui.noty_type__warning { + background-color: #fffaf3; + color: #573a08; + box-shadow: 0 0 0 1px #c9ba9b inset, 0 0 0 0 transparent; } + +.noty_theme__semanticui.noty_type__error { + background-color: #fff6f6; + color: #9f3a38; + box-shadow: 0 0 0 1px #e0b4b4 inset, 0 0 0 0 transparent; } + +.noty_theme__semanticui.noty_type__info, +.noty_theme__semanticui.noty_type__information { + background-color: #f8ffff; + color: #276f86; + box-shadow: 0 0 0 1px #a9d5de inset, 0 0 0 0 transparent; } + +.noty_theme__semanticui.noty_type__success { + background-color: #fcfff5; + color: #2c662d; + box-shadow: 0 0 0 1px #a3c293 inset, 0 0 0 0 transparent; } + +.noty_theme__nest.noty_bar { + margin: 0 0 15px 0; + overflow: hidden; + border-radius: 2px; + position: relative; + box-shadow: rgba(0, 0, 0, 0.098039) 5px 4px 10px 0; } + .noty_theme__nest.noty_bar .noty_body { + padding: 10px; + font-size: 14px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); } + .noty_theme__nest.noty_bar .noty_buttons { + padding: 10px; } + +.noty_layout .noty_theme__nest.noty_bar { + z-index: 5; } + +.noty_layout .noty_theme__nest.noty_bar:nth-child(2) { + position: absolute; + top: 0; + margin-top: 4px; + margin-right: -4px; + margin-left: 4px; + z-index: 4; + width: 100%; } + +.noty_layout .noty_theme__nest.noty_bar:nth-child(3) { + position: absolute; + top: 0; + margin-top: 8px; + margin-right: -8px; + margin-left: 8px; + z-index: 3; + width: 100%; } + +.noty_layout .noty_theme__nest.noty_bar:nth-child(4) { + position: absolute; + top: 0; + margin-top: 12px; + margin-right: -12px; + margin-left: 12px; + z-index: 2; + width: 100%; } + +.noty_layout .noty_theme__nest.noty_bar:nth-child(5) { + position: absolute; + top: 0; + margin-top: 16px; + margin-right: -16px; + margin-left: 16px; + z-index: 1; + width: 100%; } + +.noty_layout .noty_theme__nest.noty_bar:nth-child(n+6) { + position: absolute; + top: 0; + margin-top: 20px; + margin-right: -20px; + margin-left: 20px; + z-index: -1; + width: 100%; } + +#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(2), +#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(2) { + margin-top: 4px; + margin-left: -4px; + margin-right: 4px; } + +#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(3), +#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(3) { + margin-top: 8px; + margin-left: -8px; + margin-right: 8px; } + +#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(4), +#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(4) { + margin-top: 12px; + margin-left: -12px; + margin-right: 12px; } + +#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(5), +#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(5) { + margin-top: 16px; + margin-left: -16px; + margin-right: 16px; } + +#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(n+6), +#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(n+6) { + margin-top: 20px; + margin-left: -20px; + margin-right: 20px; } + +.noty_theme__nest.noty_type__alert, +.noty_theme__nest.noty_type__notification { + background-color: #073B4C; + color: #fff; } + .noty_theme__nest.noty_type__alert .noty_progressbar, + .noty_theme__nest.noty_type__notification .noty_progressbar { + background-color: #fff; } + +.noty_theme__nest.noty_type__warning { + background-color: #FFD166; + color: #fff; } + +.noty_theme__nest.noty_type__error { + background-color: #EF476F; + color: #fff; } + .noty_theme__nest.noty_type__error .noty_progressbar { + opacity: .4; } + +.noty_theme__nest.noty_type__info, +.noty_theme__nest.noty_type__information { + background-color: #118AB2; + color: #fff; } + .noty_theme__nest.noty_type__info .noty_progressbar, + .noty_theme__nest.noty_type__information .noty_progressbar { + opacity: .6; } + +.noty_theme__nest.noty_type__success { + background-color: #06D6A0; + color: #fff; } + +.noty_theme__light.noty_bar { + margin: 4px 0; + overflow: hidden; + border-radius: 2px; + position: relative; } + .noty_theme__light.noty_bar .noty_body { + padding: 10px; } + .noty_theme__light.noty_bar .noty_buttons { + border-top: 1px solid #e7e7e7; + padding: 5px 10px; } + +.noty_theme__light.noty_type__alert, +.noty_theme__light.noty_type__notification { + background-color: #fff; + border: 1px solid #dedede; + color: #444; } + +.noty_theme__light.noty_type__warning { + background-color: #FFEAA8; + border: 1px solid #FFC237; + color: #826200; } + .noty_theme__light.noty_type__warning .noty_buttons { + border-color: #dfaa30; } + +.noty_theme__light.noty_type__error { + background-color: #ED7000; + border: 1px solid #e25353; + color: #FFF; } + .noty_theme__light.noty_type__error .noty_buttons { + border-color: darkred; } + +.noty_theme__light.noty_type__info, +.noty_theme__light.noty_type__information { + background-color: #78C5E7; + border: 1px solid #3badd6; + color: #FFF; } + .noty_theme__light.noty_type__info .noty_buttons, + .noty_theme__light.noty_type__information .noty_buttons { + border-color: #0B90C4; } + +.noty_theme__light.noty_type__success { + background-color: #57C880; + border: 1px solid #7cdd77; + color: darkgreen; } + .noty_theme__light.noty_type__success .noty_buttons { + border-color: #50C24E; } + +/*# sourceMappingURL=noty.css.map*/ \ No newline at end of file diff --git a/static/noty/noty.min.js b/static/noty/noty.min.js new file mode 100644 index 000000000..4d43bf34e --- /dev/null +++ b/static/noty/noty.min.js @@ -0,0 +1,17 @@ +/* + @package NOTY - Dependency-free notification library + @version version: 3.1.4 + @contributors https://github.com/needim/noty/graphs/contributors + @documentation Examples and Documentation - http://needim.github.com/noty + @license Licensed under the MIT licenses: http://www.opensource.org/licenses/mit-license.php +*/ + +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Noty",[],e):"object"==typeof exports?exports.Noty=e():t.Noty=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=6)}([function(t,e,n){"use strict";function o(t,e,n){var o=void 0;if(!n){for(o in e)if(e.hasOwnProperty(o)&&e[o]===t)return!0}else for(o in e)if(e.hasOwnProperty(o)&&e[o]===t)return!0;return!1}function i(t){t=t||window.event,void 0!==t.stopPropagation?t.stopPropagation():t.cancelBubble=!0}function r(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e="noty_"+t+"_";return e+="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){var e=16*Math.random()|0;return("x"===t?e:3&e|8).toString(16)})}function s(t){var e=t.offsetHeight,n=window.getComputedStyle(t);return e+=parseInt(n.marginTop)+parseInt(n.marginBottom)}function u(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e=e.split(" ");for(var i=0;i=0}function c(t,e){var n=f(t),o=n+e;a(n,e)||(t.className=o.substring(1))}function l(t,e){var n=f(t),o=void 0;a(t,e)&&(o=n.replace(" "+e+" "," "),t.className=o.substring(1,o.length-1))}function d(t){t.parentNode&&t.parentNode.removeChild(t)}function f(t){return(" "+(t&&t.className||"")+" ").replace(/\s+/gi," ")}function h(){function t(){b.PageHidden=document[s],o()}function e(){b.PageHidden=!0,o()}function n(){b.PageHidden=!1,o()}function o(){b.PageHidden?i():r()}function i(){setTimeout(function(){Object.keys(b.Store).forEach(function(t){b.Store.hasOwnProperty(t)&&b.Store[t].options.visibilityControl&&b.Store[t].stop()})},100)}function r(){setTimeout(function(){Object.keys(b.Store).forEach(function(t){b.Store.hasOwnProperty(t)&&b.Store[t].options.visibilityControl&&b.Store[t].resume()}),b.queueRenderAll()},100)}var s=void 0,a=void 0;void 0!==document.hidden?(s="hidden",a="visibilitychange"):void 0!==document.msHidden?(s="msHidden",a="msvisibilitychange"):void 0!==document.webkitHidden&&(s="webkitHidden",a="webkitvisibilitychange"),a&&u(document,a,t),u(window,"blur",e),u(window,"focus",n)}function p(t){if(t.hasSound){var e=document.createElement("audio");t.options.sounds.sources.forEach(function(t){var n=document.createElement("source");n.src=t,n.type="audio/"+m(t),e.appendChild(n)}),t.barDom?t.barDom.appendChild(e):document.querySelector("body").appendChild(e),e.volume=t.options.sounds.volume,t.soundPlayed||(e.play(),t.soundPlayed=!0),e.onended=function(){d(e)}}}function m(t){return t.match(/\.([^.]+)$/)[1]}Object.defineProperty(e,"__esModule",{value:!0}),e.css=e.deepExtend=e.animationEndEvents=void 0;var v="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.inArray=o,e.stopPropagation=i,e.generateID=r,e.outerHeight=s,e.addListener=u,e.hasClass=a,e.addClass=c,e.removeClass=l,e.remove=d,e.classList=f,e.visibilityChangeFlow=h,e.createAudioElements=p;var y=n(1),b=function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e.default=t,e}(y);e.animationEndEvents="webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",e.deepExtend=function t(e){e=e||{};for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:"global",e=0,n=x;return E.hasOwnProperty(t)&&(n=E[t].maxVisible,Object.keys(P).forEach(function(n){P[n].options.queue!==t||P[n].closed||e++})),{current:e,maxVisible:n}}function i(t){E.hasOwnProperty(t.options.queue)||(E[t.options.queue]={maxVisible:x,queue:[]}),E[t.options.queue].queue.push(t)}function r(t){if(E.hasOwnProperty(t.options.queue)){var e=[];Object.keys(E[t.options.queue].queue).forEach(function(n){E[t.options.queue].queue[n].id!==t.id&&e.push(E[t.options.queue].queue[n])}),E[t.options.queue].queue=e}}function s(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"global";if(E.hasOwnProperty(t)){var e=E[t].queue.shift();e&&e.show()}}function u(){Object.keys(E).forEach(function(t){s(t)})}function a(t){var e=k.generateID("ghost"),n=document.createElement("div");n.setAttribute("id",e),k.css(n,{height:k.outerHeight(t.barDom)+"px"}),t.barDom.insertAdjacentHTML("afterend",n.outerHTML),k.remove(t.barDom),n=document.getElementById(e),k.addClass(n,"noty_fix_effects_height"),k.addListener(n,k.animationEndEvents,function(){k.remove(n)})}function c(t){m(t);var e='
'+t.options.text+"
"+d(t)+'
';t.barDom=document.createElement("div"),t.barDom.setAttribute("id",t.id),k.addClass(t.barDom,"noty_bar noty_type__"+t.options.type+" noty_theme__"+t.options.theme),t.barDom.innerHTML=e,b(t,"onTemplate")}function l(t){return!(!t.options.buttons||!Object.keys(t.options.buttons).length)}function d(t){if(l(t)){var e=document.createElement("div");return k.addClass(e,"noty_buttons"),Object.keys(t.options.buttons).forEach(function(n){e.appendChild(t.options.buttons[n].dom)}),t.options.buttons.forEach(function(t){e.appendChild(t.dom)}),e.outerHTML}return""}function f(t){t.options.modal&&(0===C&&p(),e.DocModalCount=C+=1)}function h(t){if(t.options.modal&&C>0&&(e.DocModalCount=C-=1,C<=0)){var n=document.querySelector(".noty_modal");n&&(k.removeClass(n,"noty_modal_open"),k.addClass(n,"noty_modal_close"),k.addListener(n,k.animationEndEvents,function(){k.remove(n)}))}}function p(){var t=document.querySelector("body"),e=document.createElement("div");k.addClass(e,"noty_modal"),t.insertBefore(e,t.firstChild),k.addClass(e,"noty_modal_open"),k.addListener(e,k.animationEndEvents,function(){k.removeClass(e,"noty_modal_open")})}function m(t){if(t.options.container)return void(t.layoutDom=document.querySelector(t.options.container));var e="noty_layout__"+t.options.layout;t.layoutDom=document.querySelector("div#"+e),t.layoutDom||(t.layoutDom=document.createElement("div"),t.layoutDom.setAttribute("id",e),t.layoutDom.setAttribute("role","alert"),t.layoutDom.setAttribute("aria-live","polite"),k.addClass(t.layoutDom,"noty_layout"),document.querySelector("body").appendChild(t.layoutDom))}function v(t){t.options.timeout&&(t.options.progressBar&&t.progressDom&&k.css(t.progressDom,{transition:"width "+t.options.timeout+"ms linear",width:"0%"}),clearTimeout(t.closeTimer),t.closeTimer=setTimeout(function(){t.close()},t.options.timeout))}function y(t){t.options.timeout&&t.closeTimer&&(clearTimeout(t.closeTimer),t.closeTimer=-1,t.options.progressBar&&t.progressDom&&k.css(t.progressDom,{transition:"width 0ms linear",width:"100%"}))}function b(t,e){t.listeners.hasOwnProperty(e)&&t.listeners[e].forEach(function(e){"function"==typeof e&&e.apply(t)})}function w(t){b(t,"afterShow"),v(t),k.addListener(t.barDom,"mouseenter",function(){y(t)}),k.addListener(t.barDom,"mouseleave",function(){v(t)})}function g(t){delete P[t.id],t.closing=!1,b(t,"afterClose"),k.remove(t.barDom),0!==t.layoutDom.querySelectorAll(".noty_bar").length||t.options.container||k.remove(t.layoutDom),(k.inArray("docVisible",t.options.titleCount.conditions)||k.inArray("docHidden",t.options.titleCount.conditions))&&D.decrement(),s(t.options.queue)}Object.defineProperty(e,"__esModule",{value:!0}),e.Defaults=e.Store=e.Queues=e.DefaultMaxVisible=e.docTitle=e.DocModalCount=e.PageHidden=void 0,e.getQueueCounts=o,e.addToQueue=i,e.removeFromQueue=r,e.queueRender=s,e.queueRenderAll=u,e.ghostFix=a,e.build=c,e.hasButtons=l,e.handleModal=f,e.handleModalClose=h,e.queueClose=v,e.dequeueClose=y,e.fire=b,e.openFlow=w,e.closeFlow=g;var _=n(0),k=function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e.default=t,e}(_),C=(e.PageHidden=!1,e.DocModalCount=0),S={originalTitle:null,count:0,changed:!1,timer:-1},D=e.docTitle={increment:function(){S.count++,D._update()},decrement:function(){if(--S.count<=0)return void D._clear();D._update()},_update:function(){var t=document.title;S.changed?document.title="("+S.count+") "+S.originalTitle:(S.originalTitle=t,document.title="("+S.count+") "+t,S.changed=!0)},_clear:function(){S.changed&&(S.count=0,document.title=S.originalTitle,S.changed=!1)}},x=e.DefaultMaxVisible=5,E=e.Queues={global:{maxVisible:x,queue:[]}},P=e.Store={};e.Defaults={type:"alert",layout:"topRight",theme:"mint",text:"",timeout:!1,progressBar:!0,closeWith:["click"],animation:{open:"noty_effects_open",close:"noty_effects_close"},id:!1,force:!1,killer:!1,queue:"global",container:!1,buttons:[],callbacks:{beforeShow:null,onShow:null,afterShow:null,onClose:null,afterClose:null,onClick:null,onHover:null,onTemplate:null},sounds:{sources:[],volume:1,conditions:[]},titleCount:{conditions:[]},modal:!1,visibilityControl:!1}},function(t,e,n){"use strict";function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0}),e.NotyButton=void 0;var i=n(0),r=function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e.default=t,e}(i);e.NotyButton=function t(e,n,i){var s=this,u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return o(this,t),this.dom=document.createElement("button"),this.dom.innerHTML=e,this.id=u.id=u.id||r.generateID("button"),this.cb=i,Object.keys(u).forEach(function(t){s.dom.setAttribute(t,u[t])}),r.addClass(this.dom,n||"noty_btn"),this}},function(t,e,n){"use strict";function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var i=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:"/service-worker.js";return o(this,t),this.subData={},this.workerPath=e,this.listeners={onPermissionGranted:[],onPermissionDenied:[],onSubscriptionSuccess:[],onSubscriptionCancel:[],onWorkerError:[],onWorkerSuccess:[],onWorkerNotSupported:[]},this}return i(t,[{key:"on",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){};return"function"==typeof e&&this.listeners.hasOwnProperty(t)&&this.listeners[t].push(e),this}},{key:"fire",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[];this.listeners.hasOwnProperty(t)&&this.listeners[t].forEach(function(t){"function"==typeof t&&t.apply(e,n)})}},{key:"create",value:function(){console.log("NOT IMPLEMENTED YET")}},{key:"isSupported",value:function(){var t=!1;try{t=window.Notification||window.webkitNotifications||navigator.mozNotification||window.external&&void 0!==window.external.msIsSiteMode()}catch(t){}return t}},{key:"getPermissionStatus",value:function(){var t="default";if(window.Notification&&window.Notification.permissionLevel)t=window.Notification.permissionLevel;else if(window.webkitNotifications&&window.webkitNotifications.checkPermission)switch(window.webkitNotifications.checkPermission()){case 1:t="default";break;case 0:t="granted";break;default:t="denied"}else window.Notification&&window.Notification.permission?t=window.Notification.permission:navigator.mozNotification?t="granted":window.external&&void 0!==window.external.msIsSiteMode()&&(t=window.external.msIsSiteMode()?"granted":"default");return t.toString().toLowerCase()}},{key:"getEndpoint",value:function(t){var e=t.endpoint,n=t.subscriptionId;return n&&-1===e.indexOf(n)&&(e+="/"+n),e}},{key:"isSWRegistered",value:function(){try{return"activated"===navigator.serviceWorker.controller.state}catch(t){return!1}}},{key:"unregisterWorker",value:function(){var t=this;"serviceWorker"in navigator&&navigator.serviceWorker.getRegistrations().then(function(e){var n=!0,o=!1,i=void 0;try{for(var r,s=e[Symbol.iterator]();!(n=(r=s.next()).done);n=!0){r.value.unregister(),t.fire("onSubscriptionCancel")}}catch(t){o=!0,i=t}finally{try{!n&&s.return&&s.return()}finally{if(o)throw i}}})}},{key:"requestSubscription",value:function(){var t=this,e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],n=this,o=this.getPermissionStatus(),i=function(o){"granted"===o?(t.fire("onPermissionGranted"),"serviceWorker"in navigator?navigator.serviceWorker.register(t.workerPath).then(function(){navigator.serviceWorker.ready.then(function(t){n.fire("onWorkerSuccess"),t.pushManager.subscribe({userVisibleOnly:e}).then(function(t){var e=t.getKey("p256dh"),o=t.getKey("auth");n.subData={endpoint:n.getEndpoint(t),p256dh:e?window.btoa(String.fromCharCode.apply(null,new Uint8Array(e))):null,auth:o?window.btoa(String.fromCharCode.apply(null,new Uint8Array(o))):null},n.fire("onSubscriptionSuccess",[n.subData])}).catch(function(t){n.fire("onWorkerError",[t])})})}):n.fire("onWorkerNotSupported")):"denied"===o&&(t.fire("onPermissionDenied"),t.unregisterWorker())};"default"===o?window.Notification&&window.Notification.requestPermission?window.Notification.requestPermission(i):window.webkitNotifications&&window.webkitNotifications.checkPermission&&window.webkitNotifications.requestPermission(i):i(o)}}]),t}()},function(t,e,n){(function(e,o){/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version 4.1.1 + */ +!function(e,n){t.exports=n()}(0,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function i(t){return"function"==typeof t}function r(t){z=t}function s(t){U=t}function u(){return void 0!==R?function(){R(c)}:a()}function a(){var t=setTimeout;return function(){return t(c,1)}}function c(){for(var t=0;t0&&void 0!==arguments[0]?arguments[0]:{};return i(this,t),this.options=c.deepExtend({},d.Defaults,e),this.id=this.options.id||c.generateID("bar"),this.closeTimer=-1,this.barDom=null,this.layoutDom=null,this.progressDom=null,this.showing=!1,this.shown=!1,this.closed=!1,this.closing=!1,this.killable=this.options.timeout||this.options.closeWith.length>0,this.hasSound=this.options.sounds.sources.length>0,this.soundPlayed=!1,this.listeners={beforeShow:[],onShow:[],afterShow:[],onClose:[],afterClose:[],onClick:[],onHover:[],onTemplate:[]},this.promises={show:null,close:null},this.on("beforeShow",this.options.callbacks.beforeShow),this.on("onShow",this.options.callbacks.onShow),this.on("afterShow",this.options.callbacks.afterShow),this.on("onClose",this.options.callbacks.onClose),this.on("afterClose",this.options.callbacks.afterClose),this.on("onClick",this.options.callbacks.onClick),this.on("onHover",this.options.callbacks.onHover),this.on("onTemplate",this.options.callbacks.onTemplate),this}return r(t,[{key:"on",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){};return"function"==typeof e&&this.listeners.hasOwnProperty(t)&&this.listeners[t].push(e),this}},{key:"show",value:function(){var e=this;!0===this.options.killer?t.closeAll():"string"==typeof this.options.killer&&t.closeAll(this.options.killer);var n=d.getQueueCounts(this.options.queue);if(n.current>=n.maxVisible||d.PageHidden&&this.options.visibilityControl)return d.addToQueue(this),d.PageHidden&&this.hasSound&&c.inArray("docHidden",this.options.sounds.conditions)&&c.createAudioElements(this),d.PageHidden&&c.inArray("docHidden",this.options.titleCount.conditions)&&d.docTitle.increment(),this;if(d.Store[this.id]=this,d.fire(this,"beforeShow"),this.showing=!0,this.closing)return this.showing=!1,this;if(d.build(this),d.handleModal(this),this.options.force?this.layoutDom.insertBefore(this.barDom,this.layoutDom.firstChild):this.layoutDom.appendChild(this.barDom),this.hasSound&&!this.soundPlayed&&c.inArray("docVisible",this.options.sounds.conditions)&&c.createAudioElements(this),c.inArray("docVisible",this.options.titleCount.conditions)&&d.docTitle.increment(),this.shown=!0,this.closed=!1,d.hasButtons(this)&&Object.keys(this.options.buttons).forEach(function(t){var n=e.barDom.querySelector("#"+e.options.buttons[t].id);c.addListener(n,"click",function(n){c.stopPropagation(n),e.options.buttons[t].cb()})}),this.progressDom=this.barDom.querySelector(".noty_progressbar"),c.inArray("click",this.options.closeWith)&&(c.addClass(this.barDom,"noty_close_with_click"),c.addListener(this.barDom,"click",function(t){c.stopPropagation(t),d.fire(e,"onClick"),e.close()},!1)),c.addListener(this.barDom,"mouseenter",function(){d.fire(e,"onHover")},!1),this.options.timeout&&c.addClass(this.barDom,"noty_has_timeout"),this.options.progressBar&&c.addClass(this.barDom,"noty_has_progressbar"),c.inArray("button",this.options.closeWith)){c.addClass(this.barDom,"noty_close_with_button");var o=document.createElement("div");c.addClass(o,"noty_close_button"),o.innerHTML="×",this.barDom.appendChild(o),c.addListener(o,"click",function(t){c.stopPropagation(t),e.close()},!1)}return d.fire(this,"onShow"),null===this.options.animation.open?this.promises.show=new u.default(function(t){t()}):"function"==typeof this.options.animation.open?this.promises.show=new u.default(this.options.animation.open.bind(this)):(c.addClass(this.barDom,this.options.animation.open),this.promises.show=new u.default(function(t){c.addListener(e.barDom,c.animationEndEvents,function(){c.removeClass(e.barDom,e.options.animation.open),t()})})),this.promises.show.then(function(){var t=e;setTimeout(function(){d.openFlow(t)},100)}),this}},{key:"stop",value:function(){return d.dequeueClose(this),this}},{key:"resume",value:function(){return d.queueClose(this),this}},{key:"setTimeout",value:function(t){function e(e){return t.apply(this,arguments)}return e.toString=function(){return t.toString()},e}(function(t){if(this.stop(),this.options.timeout=t,this.barDom){this.options.timeout?c.addClass(this.barDom,"noty_has_timeout"):c.removeClass(this.barDom,"noty_has_timeout");var e=this;setTimeout(function(){e.resume()},100)}return this})},{key:"setText",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return this.barDom&&(this.barDom.querySelector(".noty_body").innerHTML=t),e&&(this.options.text=t),this}},{key:"setType",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(this.barDom){c.classList(this.barDom).split(" ").forEach(function(t){"noty_type__"===t.substring(0,11)&&c.removeClass(e.barDom,t)}),c.addClass(this.barDom,"noty_type__"+t)}return n&&(this.options.type=t),this}},{key:"setTheme",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(this.barDom){c.classList(this.barDom).split(" ").forEach(function(t){"noty_theme__"===t.substring(0,12)&&c.removeClass(e.barDom,t)}),c.addClass(this.barDom,"noty_theme__"+t)}return n&&(this.options.theme=t),this}},{key:"close",value:function(){var t=this;return this.closed?this:this.shown?(d.fire(this,"onClose"),this.closing=!0,null===this.options.animation.close?this.promises.close=new u.default(function(t){t()}):"function"==typeof this.options.animation.close?this.promises.close=new u.default(this.options.animation.close.bind(this)):(c.addClass(this.barDom,this.options.animation.close),this.promises.close=new u.default(function(e){c.addListener(t.barDom,c.animationEndEvents,function(){t.options.force?c.remove(t.barDom):d.ghostFix(t),e()})})),this.promises.close.then(function(){d.closeFlow(t),d.handleModalClose(t)}),this.closed=!0,this):(d.removeFromQueue(this),this)}}],[{key:"closeAll",value:function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return Object.keys(d.Store).forEach(function(e){t?d.Store[e].options.queue===t&&d.Store[e].killable&&d.Store[e].close():d.Store[e].killable&&d.Store[e].close()}),this}},{key:"overrideDefaults",value:function(t){return d.Defaults=c.deepExtend({},d.Defaults,t),this}},{key:"setMaxVisible",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:d.DefaultMaxVisible,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"global";return d.Queues.hasOwnProperty(e)||(d.Queues[e]={maxVisible:t,queue:[]}),d.Queues[e].maxVisible=t,this}},{key:"button",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments[2],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return new f.NotyButton(t,e,n,o)}},{key:"version",value:function(){return"3.1.4"}},{key:"Push",value:function(t){return new h.Push(t)}}]),t}();e.default=p,c.visibilityChangeFlow(),t.exports=e.default},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function i(t){if(l===setTimeout)return setTimeout(t,0);if((l===n||!l)&&setTimeout)return l=setTimeout,setTimeout(t,0);try{return l(t,0)}catch(e){try{return l.call(null,t,0)}catch(e){return l.call(this,t,0)}}}function r(t){if(d===clearTimeout)return clearTimeout(t);if((d===o||!d)&&clearTimeout)return d=clearTimeout,clearTimeout(t);try{return d(t)}catch(e){try{return d.call(null,t)}catch(e){return d.call(this,t)}}}function s(){m&&h&&(m=!1,h.length?p=h.concat(p):v=-1,p.length&&u())}function u(){if(!m){var t=i(s);m=!0;for(var e=p.length;e;){for(h=p,p=[];++v1)for(var n=1;n + +