├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── bugbuzz ├── __init__.py ├── client.py ├── debugger.py └── packages │ ├── __init__.py │ ├── pubnub │ └── requests ├── demo.py ├── dev.sh ├── ez_setup.py ├── screencast.gif ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── helpers.py ├── integration │ ├── __init__.py │ ├── test_client.py │ └── test_debugger.py └── unit │ ├── __init__.py │ └── test_client.py ├── tox.ini └── vendor └── pubnub ├── LICENSE └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | cover 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mrs. Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | *.sublime-project 38 | *.sublime-workspace 39 | 40 | # virtualenv 41 | env 42 | 43 | # SQLite database 44 | *.db 45 | *.sqlite 46 | 47 | # Vagrant 48 | .vagrant 49 | docs/_build/ 50 | 51 | # intellij 52 | *.iml 53 | .idea/ 54 | .noseids 55 | .tddium* 56 | .elasticbeanstalk/ 57 | 58 | # Elastic Beanstalk Files 59 | .elasticbeanstalk/* 60 | !.elasticbeanstalk/*.cfg.yml 61 | !.elasticbeanstalk/*.global.yml 62 | 63 | docker/*.tar 64 | docker/*.txt 65 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/requests"] 2 | path = vendor/requests 3 | url = https://github.com/kennethreitz/requests.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git@github.com:pre-commit/pre-commit-hooks 2 | sha: 9ce45609a92f648c87b42207410386fd69a5d1e5 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: end-of-file-fixer 6 | - id: autopep8-wrapper 7 | - id: check-docstring-first 8 | - id: check-json 9 | - id: check-yaml 10 | - id: debug-statements 11 | - id: requirements-txt-fixer 12 | - id: flake8 13 | - repo: git@github.com:pre-commit/pre-commit 14 | sha: 4352d45451296934bc17494073b82bcacca3205c 15 | hooks: 16 | - id: validate_config 17 | - id: validate_manifest 18 | - repo: git@github.com:asottile/reorder_python_imports 19 | sha: aeda21eb7df6af8c9f6cd990abb086375c71c953 20 | hooks: 21 | - id: reorder-python-imports 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | env: 5 | - TOXENV=py27 6 | - TOXENV=py34 7 | # command to install dependencies 8 | install: 9 | - 'easy_install -U setuptools' 10 | - 'pip install -U pip' 11 | - 'pip install wheel' 12 | - 'pip install tox' 13 | # command to run tests 14 | script: 15 | - 'tox' 16 | - 'pip install flake8' 17 | - 'flake8 bugbuzz --ignore=E501,W293' 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Fang-Pen Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | recursive-include bugbuzz/packages/requests *.pem 3 | recursive-exclude tests * 4 | # Heck! wheel doesn't respec any exclude statement, let's remove tests 5 | # manuall while deplying. 6 | # ref: https://bitbucket.org/pypa/wheel/issue/99/cannot-exclude-directory 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bugbuzz - easy to use online debugger 2 | 3 | [![Build Status](https://travis-ci.org/fangpenlin/bugbuzz-python.svg?branch=master)](https://travis-ci.org/fangpenlin/bugbuzz-python) 4 | 5 | ![Bugbuzz demo](/screencast.gif?raw=true ) 6 | 7 | # Relative projects 8 | 9 | - [Ember.js Dashboard project](https://github.com/fangpenlin/bugbuzz-dashboard) 10 | - [Backend API server project](https://github.com/fangpenlin/bugbuzz-api) 11 | 12 | # Dashboard shortcuts 13 | 14 | Vim style shortcuts 15 | 16 | - C Continue 17 | - H Return 18 | - J Next 19 | - L Step 20 | 21 | # Usage 22 | 23 | ``` 24 | pip install bugbuzz 25 | ``` 26 | 27 | then insert following lines in your code to debug 28 | 29 | ```python 30 | import bugbuzz; bugbuzz.set_trace() 31 | ``` 32 | 33 | # Security concerns 34 | 35 | As bugbuzz providing debugging in a software-as-service manner, all source code and local variables needed will be uploaded to the server. When a debugging session created, a random secret access key will be generated, and used for encryping all source code and local variables. The access key will be passed to dashboard as a part of hash tag like this 36 | 37 | ``` 38 | http://dashboard.bugbuzz.io/#/sessions/SECsLArhHBVHF5mrtvXHVp3T?access_key= 39 | ``` 40 | 41 | With the access key, the Ember.js dashboard app can then decrypt the source code and local variables downloaded from the server. As the access key is passed as part of hash in the URL, the server cannot see it, without the access key, your source code and local variables are not visible by the server. 42 | 43 | For more details about security topic, you can also read my article [Anonymous computing: Peer-to-peer encryption with Ember.js](http://fangpenlin.com/posts/2015/05/26/anonymous-computing-peer-to-peer-encryption-with-ember-js). 44 | 45 | # Run demo 46 | 47 | To run our demo 48 | 49 | ```bash 50 | git clone --recursive git@github.com:fangpenlin/bugbuzz-python.git 51 | ``` 52 | 53 | install the project 54 | 55 | ```bash 56 | virtualenv --no-site-packages .env 57 | source .env/bin/activate 58 | pip install -e . 59 | ``` 60 | 61 | and dependency used in the `demo.py` 62 | 63 | ```bash 64 | pip install requests 65 | ``` 66 | 67 | then 68 | 69 | ```bash 70 | python demo.py 71 | ``` 72 | 73 | It will open a new tab in your browser for debugging. 74 | 75 | # Run with local API server and dashboard instead 76 | 77 | By default, bugbuzz uses `bugbuzz-api.herokuapp.com` as the API server and `dashboard.bugbuzz.io` as the dashboard. To change this behavior, you can specify environment variables 78 | 79 | - **BUGBUZZ_API**: URL for the API server 80 | - **BUGBUZZ_DASHBOARD**: URL for the dashboard 81 | 82 | For example, you are running API server and the dashboard locally at `http://localhost:9090` and `http://localhost:4200`, then you can run bugbuzz like this 83 | 84 | ```bash 85 | BUGBUZZ_API='http://localhost:9090' BUGBUZZ_DASHBOARD='http://localhost:4200' python demo.py 86 | ``` 87 | 88 | 89 | # Notice 90 | 91 | This is just a prototype, use it at your own risk 92 | -------------------------------------------------------------------------------- /bugbuzz/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | import sys 5 | 6 | __version__ = '0.1.2' 7 | 8 | 9 | def set_trace(): 10 | from .debugger import BugBuzz 11 | api_url = os.getenv('BUGBUZZ_API', 'https://bugbuzz-api.herokuapp.com') 12 | db_url = os.getenv( 13 | 'BUGBUZZ_DASHBOARD', 14 | 'http://dashboard.bugbuzz.io/' 15 | ) 16 | BugBuzz(api_url, db_url).set_trace(sys._getframe().f_back) 17 | -------------------------------------------------------------------------------- /bugbuzz/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import unicode_literals 3 | 4 | from future.standard_library import install_aliases 5 | install_aliases() 6 | 7 | import json # noqa: E402 8 | import logging # noqa: E402 9 | import os # noqa: E402 10 | import uuid # noqa: E402 11 | import queue # noqa: E402 12 | from urllib import parse as urlparse # noqa: E402 13 | 14 | from Crypto.Cipher import AES # noqa: E402 15 | 16 | from .packages import pubnub # noqa: E402 17 | from .packages import requests # noqa: E402 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | BLOCK_SIZE = 16 23 | 24 | 25 | def pkcs5_pad(data): 26 | """Do PKCS5 padding to data and return 27 | 28 | """ 29 | remain = BLOCK_SIZE - len(data) % BLOCK_SIZE 30 | return data + (remain * chr(remain).encode('latin1')) 31 | 32 | 33 | class BugBuzzClient(object): 34 | 35 | """BugBuzzClient talks to BugBuzz server and response commands 36 | 37 | """ 38 | 39 | def __init__(self, base_url): 40 | self.base_url = base_url 41 | self.running = True 42 | # requests session 43 | self.req_session = requests.Session() 44 | self.pubnub = None 45 | # debugging session ID 46 | self.session_id = None 47 | # last event timestamp 48 | self.last_timestamp = None 49 | # thread for polling events from server 50 | self.cmd_queue = queue.Queue() 51 | # generate AES encryption key 52 | self.aes_key = os.urandom(32) 53 | 54 | def _api_url(self, path): 55 | """API URL for path 56 | 57 | """ 58 | return urlparse.urljoin(self.base_url, path) 59 | 60 | def encrypt(self, content): 61 | """Encrypt a given content and return (iv, encrypted content) 62 | 63 | """ 64 | from builtins import bytes 65 | if not isinstance(content, bytes): 66 | raise TypeError('Content needs to be bytes') 67 | iv = os.urandom(16) 68 | aes = AES.new(self.aes_key, AES.MODE_CBC, iv) 69 | return iv, aes.encrypt(pkcs5_pad(content)) 70 | 71 | def start(self): 72 | # validation code is for validating given access key correct or not 73 | validation_code = uuid.uuid4().hex 74 | iv, encrypted_code = self.encrypt(validation_code.encode('latin1')) 75 | # talk to server, register debugging session 76 | resp = self.req_session.post( 77 | self._api_url('sessions'), 78 | dict( 79 | encrypted='true', 80 | validation_code=validation_code, 81 | ), 82 | files=dict( 83 | encrypted_code=('encrypted_code', encrypted_code), 84 | aes_iv=('aes_iv', iv), 85 | ), 86 | ) 87 | resp.raise_for_status() 88 | # TODO: handle error 89 | session = resp.json()['session'] 90 | self.session_id = session['id'] 91 | self.pubnub = pubnub.Pubnub( 92 | publish_key='', 93 | subscribe_key=session['pubnub_subscribe_key'], 94 | ssl_on=True, 95 | daemon=True, 96 | ) 97 | self.pubnub.subscribe( 98 | session['client_channel'], 99 | callback=self.process_event, 100 | ) 101 | 102 | def add_break(self, lineno, file_id, local_vars): 103 | """Add a break to notify user we are waiting for commands 104 | 105 | """ 106 | logger.info('Add break lineno=%s, file_id=%s', lineno, file_id) 107 | url = self._api_url('sessions/{}/breaks'.format(self.session_id)) 108 | iv, encrpyted = self.encrypt(json.dumps(local_vars).encode('latin1')) 109 | resp = self.req_session.post( 110 | url, 111 | dict( 112 | lineno=str(lineno), 113 | file_id=file_id, 114 | ), 115 | files=dict( 116 | local_vars=('local_vars', encrpyted), 117 | aes_iv=('aes_iv', iv), 118 | ), 119 | ) 120 | resp.raise_for_status() 121 | return resp.json()['break'] 122 | 123 | def upload_source(self, filename, content): 124 | """Uplaod source code to server 125 | 126 | """ 127 | url = self._api_url('sessions/{}/files'.format(self.session_id)) 128 | iv, encrpyted = self.encrypt(content) 129 | resp = self.req_session.post( 130 | url, 131 | files=dict( 132 | file=(filename, encrpyted), 133 | aes_iv=('aes_iv', iv), 134 | ), 135 | ) 136 | resp.raise_for_status() 137 | return resp.json()['file'] 138 | 139 | def process_event(self, message, channel): 140 | """Process events from the server 141 | 142 | """ 143 | event = message['event'] 144 | logger.debug('Events: %r', event) 145 | # TODO: process source code requests 146 | # TODO: process other requests 147 | self.cmd_queue.put_nowait(event) 148 | -------------------------------------------------------------------------------- /bugbuzz/debugger.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import unicode_literals 3 | 4 | from future.standard_library import install_aliases 5 | install_aliases() 6 | 7 | import base64 # noqa: E402 8 | import bdb # noqa: E402 9 | import inspect # noqa: E402 10 | import logging # noqa: E402 11 | import sys # noqa: E402 12 | import webbrowser # noqa: E402 13 | import queue # noqa: E402 14 | from urllib import parse as urlparse # noqa: E402 15 | 16 | from .client import BugBuzzClient # noqa: E402 17 | 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | __version__ = '0.1.2' 22 | 23 | BLOCK_SIZE = 16 24 | 25 | 26 | def pkcs5_pad(data): 27 | """Do PKCS5 padding to data and return 28 | 29 | """ 30 | remain = BLOCK_SIZE - len(data) % BLOCK_SIZE 31 | return data + (remain * chr(remain).encode('latin1')) 32 | 33 | 34 | class BugBuzz(bdb.Bdb, object): 35 | 36 | QUEUE_POLLING_TIMEOUT = 10 37 | 38 | VAR_VALUE_TRUNCATE_SIZE = 1024 39 | 40 | @classmethod 41 | def dump_vars(cls, vars): 42 | """Dump vars dict as name to repr string 43 | 44 | """ 45 | def strip(value): 46 | try: 47 | return repr(value)[:cls.VAR_VALUE_TRUNCATE_SIZE] 48 | except (KeyboardInterrupt, SystemExit): 49 | raise 50 | except Exception: 51 | return '' 52 | return dict((key, strip(value)) for key, value in vars.items()) 53 | 54 | def __init__(self, base_url, dashboard_url): 55 | bdb.Bdb.__init__(self) 56 | self.dashboard_url = dashboard_url 57 | self.client = BugBuzzClient(base_url) 58 | # map filename to uploaded files 59 | self.uploaded_sources = {} 60 | # current frame object 61 | self.current_frame = None 62 | 63 | def upload_source(self, frame): 64 | """Upload source code if it is not available on server yet 65 | 66 | """ 67 | # TODO: what if the filename is not unicode? 68 | filename = str(frame.f_code.co_filename) 69 | if filename in self.uploaded_sources: 70 | return self.uploaded_sources[filename] 71 | logger.info('Uploading %s', filename) 72 | source_lines, _ = inspect.findsource(frame.f_code) 73 | if source_lines: 74 | first_line = source_lines[0] 75 | if not isinstance(first_line, bytes): 76 | source_lines = map( 77 | lambda line: line.encode('utf8'), 78 | source_lines, 79 | ) 80 | uploaded = self.client.upload_source( 81 | filename=filename, 82 | content=b''.join(source_lines), 83 | ) 84 | self.uploaded_sources[filename] = uploaded 85 | return uploaded 86 | 87 | def set_trace(self, frame): 88 | self.current_frame = frame 89 | self.client.start() 90 | access_key = base64.urlsafe_b64encode(self.client.aes_key) 91 | session_url = urlparse.urljoin( 92 | self.dashboard_url, 93 | '/#/sessions/{}?{}'.format( 94 | self.client.session_id, 95 | urlparse.urlencode(dict(access_key=access_key)), 96 | ) 97 | ) 98 | print('Access Key:', access_key, file=sys.stderr) 99 | print('Dashboard URL:', session_url, file=sys.stderr) 100 | webbrowser.open_new_tab(session_url) 101 | file_ = self.upload_source(self.current_frame) 102 | # TODO: handle filename is None or other situations? 103 | self.client.add_break( 104 | file_id=file_['id'], 105 | lineno=self.current_frame.f_lineno, 106 | local_vars=self.dump_vars(self.current_frame.f_locals), 107 | ) 108 | bdb.Bdb.set_trace(self, frame) 109 | 110 | def interaction(self, frame, traceback=None): 111 | self.current_frame = frame 112 | file_ = self.upload_source(self.current_frame) 113 | # TODO: handle filename is None or other situations? 114 | self.client.add_break( 115 | file_id=file_['id'], 116 | lineno=self.current_frame.f_lineno, 117 | local_vars=self.dump_vars(self.current_frame.f_locals), 118 | ) 119 | 120 | cmd = None 121 | while cmd is None: 122 | try: 123 | # Notice: we need to specify a timeout, otherwise the get 124 | # operation cannot be interrupted 125 | cmd = self.client.cmd_queue.get( 126 | True, 127 | self.QUEUE_POLLING_TIMEOUT, 128 | ) 129 | except (KeyboardInterrupt, SystemExit): 130 | raise 131 | except queue.Empty: 132 | continue 133 | cmd_type = cmd['type'] 134 | if cmd_type == 'return': 135 | self.set_return(frame) 136 | elif cmd_type == 'next': 137 | self.set_next(frame) 138 | elif cmd_type == 'step': 139 | self.set_step() 140 | elif cmd_type == 'continue': 141 | self.set_continue() 142 | 143 | def user_call(self, frame, argument_list): 144 | """This method is called when there is the remote possibility 145 | that we ever need to stop in this function. 146 | 147 | """ 148 | logger.debug( 149 | 'User call, %s:%s', 150 | frame.f_code.co_filename, 151 | frame.f_lineno 152 | ) 153 | self.interaction(frame) 154 | 155 | def user_line(self, frame): 156 | """This method is called when we stop or break at this line. 157 | 158 | """ 159 | logger.debug( 160 | 'User line, %s:%s', 161 | frame.f_code.co_filename, 162 | frame.f_lineno 163 | ) 164 | self.interaction(frame) 165 | 166 | def user_return(self, frame, return_value): 167 | """This method is called when a return trap is set here. 168 | 169 | """ 170 | logger.debug( 171 | 'User return, %s:%s', 172 | frame.f_code.co_filename, 173 | frame.f_lineno 174 | ) 175 | self.interaction(frame) 176 | 177 | def user_exception(self, frame, exc_info): 178 | """This method is called if an exception occurs, 179 | but only if we are to stop at or just below this level. 180 | 181 | """ 182 | logger.debug( 183 | 'User exception, %s:%s', 184 | frame.f_code.co_filename, 185 | frame.f_lineno 186 | ) 187 | exc_type, exc_value, exc_traceback = exc_info 188 | self.interaction(frame, exc_traceback) 189 | -------------------------------------------------------------------------------- /bugbuzz/packages/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from . import pubnub # noqa 4 | from . import requests # noqa 5 | -------------------------------------------------------------------------------- /bugbuzz/packages/pubnub: -------------------------------------------------------------------------------- 1 | ../../vendor/pubnub -------------------------------------------------------------------------------- /bugbuzz/packages/requests: -------------------------------------------------------------------------------- 1 | ../../vendor/requests/requests -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | import bugbuzz 4 | bugbuzz.set_trace() 5 | 6 | for url in ['http://google.com', 'http://facebook.com', 'http://twitter.com']: 7 | requests.get(url) 8 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | export BUGBUZZ_API=http://localhost:9090 2 | export BUGBUZZ_DASHBOARD=http://localhost:4200 3 | -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Bootstrap setuptools installation 3 | 4 | To use setuptools in your package's setup.py, include this 5 | file in the same directory and add this to the top of your setup.py:: 6 | 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | 10 | To require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, simply supply 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import shutil 18 | import sys 19 | import tempfile 20 | import zipfile 21 | import optparse 22 | import subprocess 23 | import platform 24 | import textwrap 25 | import contextlib 26 | 27 | from distutils import log 28 | 29 | try: 30 | from site import USER_SITE 31 | except ImportError: 32 | USER_SITE = None 33 | 34 | DEFAULT_VERSION = "3.5.1" 35 | DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" 36 | 37 | 38 | def _python_cmd(*args): 39 | """ 40 | Return True if the command succeeded. 41 | """ 42 | args = (sys.executable,) + args 43 | return subprocess.call(args) == 0 44 | 45 | 46 | def _install(archive_filename, install_args=()): 47 | with archive_context(archive_filename): 48 | # installing 49 | log.warn('Installing Setuptools') 50 | if not _python_cmd('setup.py', 'install', *install_args): 51 | log.warn('Something went wrong during the installation.') 52 | log.warn('See the error message above.') 53 | # exitcode will be 2 54 | return 2 55 | 56 | 57 | def _build_egg(egg, archive_filename, to_dir): 58 | with archive_context(archive_filename): 59 | # building an egg 60 | log.warn('Building a Setuptools egg in %s', to_dir) 61 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 62 | # returning the result 63 | log.warn(egg) 64 | if not os.path.exists(egg): 65 | raise IOError('Could not build the egg.') 66 | 67 | 68 | def get_zip_class(): 69 | """ 70 | Supplement ZipFile class to support context manager for Python 2.6 71 | """ 72 | class ContextualZipFile(zipfile.ZipFile): 73 | def __enter__(self): 74 | return self 75 | 76 | def __exit__(self, type, value, traceback): 77 | self.close 78 | return zipfile.ZipFile if hasattr(zipfile.ZipFile, '__exit__') else \ 79 | ContextualZipFile 80 | 81 | 82 | @contextlib.contextmanager 83 | def archive_context(filename): 84 | # extracting the archive 85 | tmpdir = tempfile.mkdtemp() 86 | log.warn('Extracting in %s', tmpdir) 87 | old_wd = os.getcwd() 88 | try: 89 | os.chdir(tmpdir) 90 | with get_zip_class()(filename) as archive: 91 | archive.extractall() 92 | 93 | # going in the directory 94 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 95 | os.chdir(subdir) 96 | log.warn('Now working in %s', subdir) 97 | yield 98 | 99 | finally: 100 | os.chdir(old_wd) 101 | shutil.rmtree(tmpdir) 102 | 103 | 104 | def _do_download(version, download_base, to_dir, download_delay): 105 | egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' 106 | % (version, sys.version_info[0], sys.version_info[1])) 107 | if not os.path.exists(egg): 108 | archive = download_setuptools(version, download_base, 109 | to_dir, download_delay) 110 | _build_egg(egg, archive, to_dir) 111 | sys.path.insert(0, egg) 112 | 113 | # Remove previously-imported pkg_resources if present (see 114 | # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). 115 | if 'pkg_resources' in sys.modules: 116 | del sys.modules['pkg_resources'] 117 | 118 | import setuptools 119 | setuptools.bootstrap_install_from = egg 120 | 121 | 122 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 123 | to_dir=os.curdir, download_delay=15): 124 | to_dir = os.path.abspath(to_dir) 125 | rep_modules = 'pkg_resources', 'setuptools' 126 | imported = set(sys.modules).intersection(rep_modules) 127 | try: 128 | import pkg_resources 129 | except ImportError: 130 | return _do_download(version, download_base, to_dir, download_delay) 131 | try: 132 | pkg_resources.require("setuptools>=" + version) 133 | return 134 | except pkg_resources.DistributionNotFound: 135 | return _do_download(version, download_base, to_dir, download_delay) 136 | except pkg_resources.VersionConflict as VC_err: 137 | if imported: 138 | msg = textwrap.dedent(""" 139 | The required version of setuptools (>={version}) is not available, 140 | and can't be installed while this script is running. Please 141 | install a more recent version first, using 142 | 'easy_install -U setuptools'. 143 | 144 | (Currently using {VC_err.args[0]!r}) 145 | """).format(VC_err=VC_err, version=version) 146 | sys.stderr.write(msg) 147 | sys.exit(2) 148 | 149 | # otherwise, reload ok 150 | del pkg_resources, sys.modules['pkg_resources'] 151 | return _do_download(version, download_base, to_dir, download_delay) 152 | 153 | 154 | def _clean_check(cmd, target): 155 | """ 156 | Run the command to download target. If the command fails, clean up before 157 | re-raising the error. 158 | """ 159 | try: 160 | subprocess.check_call(cmd) 161 | except subprocess.CalledProcessError: 162 | if os.access(target, os.F_OK): 163 | os.unlink(target) 164 | raise 165 | 166 | 167 | def download_file_powershell(url, target): 168 | """ 169 | Download the file at url to target using Powershell (which will validate 170 | trust). Raise an exception if the command cannot complete. 171 | """ 172 | target = os.path.abspath(target) 173 | cmd = [ 174 | 'powershell', 175 | '-Command', 176 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), 177 | ] 178 | _clean_check(cmd, target) 179 | 180 | 181 | def has_powershell(): 182 | if platform.system() != 'Windows': 183 | return False 184 | cmd = ['powershell', '-Command', 'echo test'] 185 | devnull = open(os.path.devnull, 'wb') 186 | try: 187 | try: 188 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 189 | except Exception: 190 | return False 191 | finally: 192 | devnull.close() 193 | return True 194 | 195 | download_file_powershell.viable = has_powershell 196 | 197 | 198 | def download_file_curl(url, target): 199 | cmd = ['curl', url, '--silent', '--output', target] 200 | _clean_check(cmd, target) 201 | 202 | 203 | def has_curl(): 204 | cmd = ['curl', '--version'] 205 | devnull = open(os.path.devnull, 'wb') 206 | try: 207 | try: 208 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 209 | except Exception: 210 | return False 211 | finally: 212 | devnull.close() 213 | return True 214 | 215 | download_file_curl.viable = has_curl 216 | 217 | 218 | def download_file_wget(url, target): 219 | cmd = ['wget', url, '--quiet', '--output-document', target] 220 | _clean_check(cmd, target) 221 | 222 | 223 | def has_wget(): 224 | cmd = ['wget', '--version'] 225 | devnull = open(os.path.devnull, 'wb') 226 | try: 227 | try: 228 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull) 229 | except Exception: 230 | return False 231 | finally: 232 | devnull.close() 233 | return True 234 | 235 | download_file_wget.viable = has_wget 236 | 237 | 238 | def download_file_insecure(url, target): 239 | """ 240 | Use Python to download the file, even though it cannot authenticate the 241 | connection. 242 | """ 243 | try: 244 | from urllib.request import urlopen 245 | except ImportError: 246 | from urllib2 import urlopen 247 | src = dst = None 248 | try: 249 | src = urlopen(url) 250 | # Read/write all in one block, so we don't create a corrupt file 251 | # if the download is interrupted. 252 | data = src.read() 253 | dst = open(target, "wb") 254 | dst.write(data) 255 | finally: 256 | if src: 257 | src.close() 258 | if dst: 259 | dst.close() 260 | 261 | download_file_insecure.viable = lambda: True 262 | 263 | 264 | def get_best_downloader(): 265 | downloaders = [ 266 | download_file_powershell, 267 | download_file_curl, 268 | download_file_wget, 269 | download_file_insecure, 270 | ] 271 | 272 | for dl in downloaders: 273 | if dl.viable(): 274 | return dl 275 | 276 | 277 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 278 | to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): 279 | """ 280 | Download setuptools from a specified location and return its filename 281 | 282 | `version` should be a valid setuptools version number that is available 283 | as an egg for download under the `download_base` URL (which should end 284 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 285 | `delay` is the number of seconds to pause before an actual download 286 | attempt. 287 | 288 | ``downloader_factory`` should be a function taking no arguments and 289 | returning a function for downloading a URL to a target. 290 | """ 291 | # making sure we use the absolute path 292 | to_dir = os.path.abspath(to_dir) 293 | zip_name = "setuptools-%s.zip" % version 294 | url = download_base + zip_name 295 | saveto = os.path.join(to_dir, zip_name) 296 | if not os.path.exists(saveto): # Avoid repeated downloads 297 | log.warn("Downloading %s", url) 298 | downloader = downloader_factory() 299 | downloader(url, saveto) 300 | return os.path.realpath(saveto) 301 | 302 | 303 | def _build_install_args(options): 304 | """ 305 | Build the arguments to 'python setup.py install' on the setuptools package 306 | """ 307 | return ['--user'] if options.user_install else [] 308 | 309 | 310 | def _parse_args(): 311 | """ 312 | Parse the command line for options 313 | """ 314 | parser = optparse.OptionParser() 315 | parser.add_option( 316 | '--user', dest='user_install', action='store_true', default=False, 317 | help='install in user site package (requires Python 2.6 or later)') 318 | parser.add_option( 319 | '--download-base', dest='download_base', metavar="URL", 320 | default=DEFAULT_URL, 321 | help='alternative URL from where to download the setuptools package') 322 | parser.add_option( 323 | '--insecure', dest='downloader_factory', action='store_const', 324 | const=lambda: download_file_insecure, default=get_best_downloader, 325 | help='Use internal, non-validating downloader' 326 | ) 327 | parser.add_option( 328 | '--version', help="Specify which version to download", 329 | default=DEFAULT_VERSION, 330 | ) 331 | options, args = parser.parse_args() 332 | # positional arguments are ignored 333 | return options 334 | 335 | 336 | def main(): 337 | """Install or upgrade setuptools and EasyInstall""" 338 | options = _parse_args() 339 | archive = download_setuptools( 340 | version=options.version, 341 | download_base=options.download_base, 342 | downloader_factory=options.downloader_factory, 343 | ) 344 | return _install(archive, _build_install_args(options)) 345 | 346 | if __name__ == '__main__': 347 | sys.exit(main()) 348 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fangpenlin/bugbuzz-python/f1ba1ba8b6c22a60f3baac57ebfc2683bb2ec117/screencast.gif -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from ez_setup import use_setuptools 2 | use_setuptools() 3 | 4 | from setuptools import setup, find_packages # noqa: E402 5 | 6 | version = '0.0.0' 7 | try: 8 | from bugbuzz import __version__ 9 | version = __version__ 10 | except ImportError: 11 | pass 12 | 13 | 14 | tests_require = [ 15 | 'mock', 16 | 'pytest', 17 | 'pytest-cov', 18 | 'pytest-xdist', 19 | 'pytest-capturelog', 20 | 'pytest-mock', 21 | ] 22 | 23 | setup( 24 | name='bugbuzz', 25 | author='Fang-Pen Lin', 26 | author_email='hello@fangpenlin.com', 27 | url='https://github.com/fangpenlin/bugbuzz-python', 28 | description='Easy to use web-base online debugger', 29 | classifiers=[ 30 | "Programming Language :: Python :: 2", 31 | "Programming Language :: Python :: 3", 32 | "License :: OSI Approved :: MIT License", 33 | "Intended Audience :: Developers", 34 | "Programming Language :: Python", 35 | "Topic :: Software Development :: Debuggers", 36 | ], 37 | keywords='debugger debug pdb', 38 | license='MIT', 39 | version=version, 40 | packages=find_packages(exclude=('tests', )), 41 | package_data={'': ['LICENSE'], 'bugbuzz/packages/requests': ['*.pem']}, 42 | include_package_data=True, 43 | zip_safe=False, 44 | install_requires=[ 45 | 'pycrypto', 46 | 'future', 47 | ], 48 | extras_require=dict( 49 | tests=tests_require, 50 | ), 51 | tests_require=tests_require, 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fangpenlin/bugbuzz-python/f1ba1ba8b6c22a60f3baac57ebfc2683bb2ec117/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from builtins import bytes 2 | 3 | 4 | def pkcs5_unpad(data): 5 | """Do PKCS5 unpadding to data and return 6 | 7 | """ 8 | data_bytes = bytes(data) 9 | return data_bytes[0:-data_bytes[-1]] 10 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fangpenlin/bugbuzz-python/f1ba1ba8b6c22a60f3baac57ebfc2683bb2ec117/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/test_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from future.standard_library import install_aliases 4 | install_aliases() 5 | 6 | import json 7 | import base64 8 | from urllib import parse as urlparse 9 | 10 | import pytest 11 | from Crypto.Cipher import AES 12 | 13 | from bugbuzz.client import BugBuzzClient 14 | from bugbuzz.packages import requests 15 | from ..helpers import pkcs5_unpad 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | def bugbuzz_client(base_url='https://bugbuzz-api.herokuapp.com'): 20 | client = BugBuzzClient(base_url) 21 | client.start() 22 | return client 23 | 24 | 25 | @pytest.fixture(scope='session') 26 | def source_file(bugbuzz_client): 27 | with open(__file__, 'rb') as source_file: 28 | source = source_file.read() 29 | uploaded_file = bugbuzz_client.upload_source(__file__, source) 30 | return source, uploaded_file 31 | 32 | 33 | def test_creating_session(bugbuzz_client): 34 | bugbuzz_client.start() 35 | assert bugbuzz_client.session_id.startswith('SE') 36 | url = urlparse.urljoin( 37 | bugbuzz_client.base_url, 38 | '/sessions/{}'.format(bugbuzz_client.session_id), 39 | ) 40 | resp = requests.get(url) 41 | session = resp.json()['session'] 42 | assert session['encrypted'] 43 | assert session.get('validation_code') 44 | assert session.get('aes_iv') 45 | 46 | 47 | def test_upload_source(bugbuzz_client, source_file): 48 | source, uploaded_file = source_file 49 | 50 | url = urlparse.urljoin( 51 | bugbuzz_client.base_url, 52 | '/files/{}'.format(uploaded_file['id']), 53 | ) 54 | resp = requests.get(url) 55 | file_ = resp.json()['file'] 56 | assert file_['session'] == bugbuzz_client.session_id 57 | 58 | iv = base64.b64decode(file_['aes_iv'].encode('latin1')) 59 | content = base64.b64decode(file_['content'].encode('latin1')) 60 | aes = AES.new(bugbuzz_client.aes_key, AES.MODE_CBC, iv) 61 | assert pkcs5_unpad(aes.decrypt(content)) == source 62 | 63 | 64 | def test_add_break(bugbuzz_client, source_file): 65 | source, uploaded_file = source_file 66 | local_vars = dict(foo='bar', eggs='spam') 67 | created_break_ = bugbuzz_client.add_break( 68 | 73, 69 | uploaded_file['id'], 70 | local_vars, 71 | ) 72 | 73 | url = urlparse.urljoin( 74 | bugbuzz_client.base_url, 75 | '/breaks/{}'.format(created_break_['id']), 76 | ) 77 | resp = requests.get(url) 78 | break_ = resp.json()['break'] 79 | assert break_['session'] == bugbuzz_client.session_id 80 | assert break_['lineno'] == 73 81 | assert break_['file'] == uploaded_file['id'] 82 | 83 | iv = base64.b64decode(break_['aes_iv'].encode('latin1')) 84 | encrypted = base64.b64decode(break_['local_vars'].encode('latin1')) 85 | aes = AES.new(bugbuzz_client.aes_key, AES.MODE_CBC, iv) 86 | parsed_local_vars = json.loads( 87 | pkcs5_unpad(aes.decrypt(encrypted)).decode('latin1') 88 | ) 89 | assert parsed_local_vars == local_vars 90 | -------------------------------------------------------------------------------- /tests/integration/test_debugger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from future.standard_library import install_aliases 5 | install_aliases() 6 | 7 | import sys 8 | import time 9 | import threading 10 | from urllib import parse as urlparse 11 | 12 | import pytest 13 | 14 | from bugbuzz.debugger import BugBuzz 15 | from bugbuzz.packages import requests 16 | 17 | # just a dummy unicode string, to see if we can handle unicode correctly 18 | DUMMY_STR = u'除錯' 19 | 20 | 21 | @pytest.fixture(scope='session') 22 | def bugbuzz_dbg( 23 | base_url='https://bugbuzz-api.herokuapp.com', 24 | dashboard_url='http://dashboard.bugbuzz.io/', 25 | ): 26 | return BugBuzz(base_url, dashboard_url) 27 | 28 | 29 | def test_set_trace(mocker, bugbuzz_dbg): 30 | mocker.patch('webbrowser.open_new_tab') 31 | base_url = bugbuzz_dbg.client.base_url 32 | 33 | # post continue command 34 | def post_continue(): 35 | time.sleep(3) 36 | url = urlparse.urljoin( 37 | base_url, 38 | '/sessions/{}/actions/continue'.format( 39 | bugbuzz_dbg.client.session_id 40 | ), 41 | ) 42 | resp = requests.post(url) 43 | resp.raise_for_status() 44 | 45 | thread = threading.Thread(target=post_continue) 46 | thread.daemon = True 47 | thread.start() 48 | 49 | # TODO: set a timeout here? 50 | bugbuzz_dbg.set_trace(sys._getframe()) 51 | 52 | sid = bugbuzz_dbg.client.session_id 53 | url = urlparse.urljoin(base_url, '/sessions/{}'.format(sid)) 54 | resp = requests.get(url) 55 | session = resp.json()['session'] 56 | assert len(session['files']) == 1 57 | assert len(session['breaks']) == 2 58 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fangpenlin/bugbuzz-python/f1ba1ba8b6c22a60f3baac57ebfc2683bb2ec117/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import pytest 4 | from Crypto.Cipher import AES 5 | 6 | from ..helpers import pkcs5_unpad 7 | from bugbuzz.client import BugBuzzClient 8 | 9 | 10 | @pytest.fixture 11 | def bugbuzz_client(base_url='http://localhost'): 12 | return BugBuzzClient(base_url) 13 | 14 | 15 | def test_random_access_key(): 16 | keys = set() 17 | for _ in range(100): 18 | client = bugbuzz_client() 19 | keys.add(client.aes_key) 20 | assert len(keys) == 100 21 | 22 | 23 | def test_encrypt_decrypt(bugbuzz_client): 24 | plaintext = b'super foobar' 25 | iv, encrypted = bugbuzz_client.encrypt(plaintext) 26 | assert encrypted != plaintext 27 | 28 | aes = AES.new(bugbuzz_client.aes_key, AES.MODE_CBC, iv) 29 | assert pkcs5_unpad(aes.decrypt(encrypted)) == plaintext 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34} 3 | 4 | [testenv] 5 | usedevelop = True 6 | deps = 7 | -e.[tests] 8 | commands=py.test {posargs:-svvv --cov-report term-missing --cov bugbuzz tests} 9 | -------------------------------------------------------------------------------- /vendor/pubnub/LICENSE: -------------------------------------------------------------------------------- 1 | PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks 2 | Copyright (c) 2013 PubNub Inc. 3 | http://www.pubnub.com/ 4 | http://www.pubnub.com/terms 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks 25 | Copyright (c) 2013 PubNub Inc. 26 | http://www.pubnub.com/ 27 | http://www.pubnub.com/terms -------------------------------------------------------------------------------- /vendor/pubnub/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | ## www.pubnub.com - PubNub Real-time push service in the cloud. 3 | # coding=utf8 4 | 5 | ## PubNub Real-time Push APIs and Notifications Framework 6 | ## Copyright (c) 2014-15 Stephen Blum 7 | ## http://www.pubnub.com/ 8 | 9 | ## ----------------------------------- 10 | ## PubNub 3.7.1 Real-time Push Cloud API 11 | ## ----------------------------------- 12 | 13 | 14 | try: 15 | import json 16 | except ImportError: 17 | import simplejson as json 18 | 19 | import time 20 | import hashlib 21 | import uuid as uuid_lib 22 | import random 23 | import sys 24 | from base64 import urlsafe_b64encode 25 | from base64 import encodestring, decodestring 26 | import hmac 27 | 28 | 29 | try: 30 | from hashlib import sha256 31 | digestmod = sha256 32 | except ImportError: 33 | import Crypto.Hash.SHA256 as digestmod 34 | sha256 = digestmod.new 35 | 36 | 37 | ##### vanilla python imports ##### 38 | try: 39 | from urllib.parse import quote 40 | except ImportError: 41 | from urllib2 import quote 42 | try: 43 | import urllib.request 44 | except ImportError: 45 | import urllib2 46 | 47 | try: 48 | from .. import requests 49 | from ..requests.adapters import HTTPAdapter 50 | except ImportError: 51 | pass 52 | 53 | import socket 54 | import sys 55 | import threading 56 | from threading import current_thread 57 | 58 | try: 59 | import urllib3.HTTPConnection 60 | default_socket_options = urllib3.HTTPConnection.default_socket_options 61 | except: 62 | default_socket_options = [] 63 | 64 | default_socket_options += [ 65 | # Enable TCP keepalive 66 | (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 67 | ] 68 | 69 | if sys.platform.startswith("linux"): 70 | default_socket_options += [ 71 | # Send first keepalive packet 200 seconds after last data packet 72 | (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 200), 73 | # Resend keepalive packets every second, when unanswered 74 | (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1), 75 | # Close the socket after 5 unanswered keepalive packets 76 | (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) 77 | ] 78 | elif sys.platform.startswith("darwin"): 79 | # From /usr/include/netinet/tcp.h 80 | socket.TCP_KEEPALIVE = 0x10 # idle time used when SO_KEEPALIVE is enabled 81 | 82 | default_socket_options += [ 83 | # Send first keepalive packet 200 seconds after last data packet 84 | (socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 200), 85 | # Resend keepalive packets every second, when unanswered 86 | #(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1), 87 | # Close the socket after 5 unanswered keepalive packets 88 | #(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) 89 | ] 90 | """ 91 | # The Windows code is currently untested 92 | elif sys.platform.startswith("win"): 93 | import struct 94 | from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool 95 | 96 | def patch_socket_keepalive(conn): 97 | conn.sock.ioctl(socket.SIO_KEEPALIVE_VALS, ( 98 | # Enable TCP keepalive 99 | 1, 100 | # Send first keepalive packet 200 seconds after last data packet 101 | 200, 102 | # Resend keepalive packets every second, when unanswered 103 | 1 104 | )) 105 | 106 | class PubnubHTTPConnectionPool(HTTPConnectionPool): 107 | def _validate_conn(self, conn): 108 | super(PubnubHTTPConnectionPool, self)._validate_conn(conn) 109 | 110 | class PubnubHTTPSConnectionPool(HTTPSConnectionPool): 111 | def _validate_conn(self, conn): 112 | super(PubnubHTTPSConnectionPool, self)._validate_conn(conn) 113 | 114 | import urllib3.poolmanager 115 | urllib3.poolmanager.pool_classes_by_scheme = { 116 | 'http' : PubnubHTTPConnectionPool, 117 | 'https' : PubnubHTTPSConnectionPool 118 | } 119 | """ 120 | 121 | ################################## 122 | 123 | 124 | ##### Tornado imports and globals ##### 125 | try: 126 | import tornado.httpclient 127 | import tornado.ioloop 128 | from tornado.stack_context import ExceptionStackContext 129 | ioloop = tornado.ioloop.IOLoop.instance() 130 | except ImportError: 131 | pass 132 | 133 | ####################################### 134 | 135 | 136 | ##### Twisted imports and globals ##### 137 | try: 138 | from twisted.web.client import getPage 139 | from twisted.internet import reactor 140 | from twisted.internet.defer import Deferred 141 | from twisted.internet.protocol import Protocol 142 | from twisted.web.client import Agent, ContentDecoderAgent 143 | from twisted.web.client import RedirectAgent, GzipDecoder 144 | from twisted.web.client import HTTPConnectionPool 145 | from twisted.web.http_headers import Headers 146 | from twisted.internet.ssl import ClientContextFactory 147 | from twisted.internet.task import LoopingCall 148 | import twisted 149 | 150 | from twisted.python.compat import ( 151 | _PY3, unicode, intToBytes, networkString, nativeString) 152 | 153 | pnconn_pool = HTTPConnectionPool(reactor, persistent=True) 154 | pnconn_pool.maxPersistentPerHost = 100000 155 | pnconn_pool.cachedConnectionTimeout = 15 156 | pnconn_pool.retryAutomatically = True 157 | 158 | class WebClientContextFactory(ClientContextFactory): 159 | def getContext(self, hostname, port): 160 | return ClientContextFactory.getContext(self) 161 | 162 | class PubNubPamResponse(Protocol): 163 | def __init__(self, finished): 164 | self.finished = finished 165 | 166 | def dataReceived(self, bytes): 167 | self.finished.callback(bytes) 168 | 169 | class PubNubResponse(Protocol): 170 | def __init__(self, finished): 171 | self.finished = finished 172 | 173 | def dataReceived(self, bytes): 174 | self.finished.callback(bytes) 175 | except ImportError: 176 | pass 177 | 178 | 179 | ####################################### 180 | 181 | 182 | def get_data_for_user(data): 183 | try: 184 | if 'message' in data and 'payload' in data: 185 | return {'message': data['message'], 'payload': data['payload']} 186 | else: 187 | return data 188 | except TypeError: 189 | return data 190 | 191 | 192 | class PubnubCrypto2(): 193 | 194 | def pad(self, msg, block_size=16): 195 | 196 | padding = block_size - (len(msg) % block_size) 197 | return msg + chr(padding) * padding 198 | 199 | def depad(self, msg): 200 | 201 | return msg[0:-ord(msg[-1])] 202 | 203 | def getSecret(self, key): 204 | 205 | return hashlib.sha256(key).hexdigest() 206 | 207 | def encrypt(self, key, msg): 208 | from Crypto.Cipher import AES 209 | secret = self.getSecret(key) 210 | Initial16bytes = '0123456789012345' 211 | cipher = AES.new(secret[0:32], AES.MODE_CBC, Initial16bytes) 212 | enc = encodestring(cipher.encrypt(self.pad(msg))) 213 | return enc 214 | 215 | def decrypt(self, key, msg): 216 | from Crypto.Cipher import AES 217 | try: 218 | secret = self.getSecret(key) 219 | Initial16bytes = '0123456789012345' 220 | cipher = AES.new(secret[0:32], AES.MODE_CBC, Initial16bytes) 221 | plain = self.depad(cipher.decrypt(decodestring(msg))) 222 | except: 223 | return msg 224 | try: 225 | return eval(plain) 226 | except SyntaxError: 227 | return plain 228 | 229 | class PubnubCrypto3(): 230 | 231 | def pad(self, msg, block_size=16): 232 | 233 | padding = block_size - (len(msg) % block_size) 234 | return msg + (chr(padding) * padding).encode('utf-8') 235 | 236 | def depad(self, msg): 237 | 238 | return msg[0:-ord(msg[-1])] 239 | 240 | def getSecret(self, key): 241 | 242 | return hashlib.sha256(key.encode("utf-8")).hexdigest() 243 | 244 | def encrypt(self, key, msg): 245 | from Crypto.Cipher import AES 246 | secret = self.getSecret(key) 247 | Initial16bytes = '0123456789012345' 248 | cipher = AES.new(secret[0:32], AES.MODE_CBC, Initial16bytes) 249 | return encodestring( 250 | cipher.encrypt(self.pad(msg.encode('utf-8')))).decode('utf-8') 251 | 252 | def decrypt(self, key, msg): 253 | from Crypto.Cipher import AES 254 | secret = self.getSecret(key) 255 | Initial16bytes = '0123456789012345' 256 | cipher = AES.new(secret[0:32], AES.MODE_CBC, Initial16bytes) 257 | return (cipher.decrypt( 258 | decodestring(msg.encode('utf-8')))).decode('utf-8') 259 | 260 | 261 | class PubnubBase(object): 262 | def __init__( 263 | self, 264 | publish_key, 265 | subscribe_key, 266 | secret_key=False, 267 | cipher_key=False, 268 | auth_key=None, 269 | ssl_on=False, 270 | origin='pubsub.pubnub.com', 271 | uuid=None 272 | ): 273 | """Pubnub Class 274 | 275 | Provides methods to communicate with Pubnub cloud 276 | 277 | Attributes: 278 | publish_key: Publish Key 279 | subscribe_key: Subscribe Key 280 | secret_key: Secret Key 281 | cipher_key: Cipher Key 282 | auth_key: Auth Key (used with Pubnub Access Manager i.e. PAM) 283 | ssl: SSL enabled ? 284 | origin: Origin 285 | """ 286 | 287 | self.origin = origin 288 | self.version = '3.7.1' 289 | self.limit = 1800 290 | self.publish_key = publish_key 291 | self.subscribe_key = subscribe_key 292 | self.secret_key = secret_key 293 | self.cipher_key = cipher_key 294 | self.ssl = ssl_on 295 | self.auth_key = auth_key 296 | 297 | if self.ssl: 298 | self.origin = 'https://' + self.origin 299 | else: 300 | self.origin = 'http://' + self.origin 301 | 302 | self.uuid = uuid or str(uuid_lib.uuid4()) 303 | 304 | if type(sys.version_info) is tuple: 305 | self.python_version = 2 306 | self.pc = PubnubCrypto2() 307 | else: 308 | if sys.version_info.major == 2: 309 | self.python_version = 2 310 | self.pc = PubnubCrypto2() 311 | else: 312 | self.python_version = 3 313 | self.pc = PubnubCrypto3() 314 | 315 | if not isinstance(self.uuid, str): 316 | raise AttributeError("uuid must be a string") 317 | 318 | def _pam_sign(self, msg): 319 | 320 | return urlsafe_b64encode(hmac.new( 321 | self.secret_key.encode("utf-8"), 322 | msg.encode("utf-8"), 323 | sha256 324 | ).digest()) 325 | 326 | def set_u(self, u=False): 327 | self.u = u 328 | 329 | def _pam_auth(self, query, apicode=0, callback=None, error=None): 330 | 331 | if 'timestamp' not in query: 332 | query['timestamp'] = int(time.time()) 333 | 334 | ## Global Grant? 335 | if 'auth' in query and not query['auth']: 336 | del query['auth'] 337 | 338 | if 'channel' in query and not query['channel']: 339 | del query['channel'] 340 | 341 | if 'channel-group' in query and not query['channel-group']: 342 | del query['channel-group'] 343 | 344 | 345 | params = "&".join([ 346 | x + "=" + quote( 347 | str(query[x]), safe="" 348 | ) for x in sorted(query) 349 | ]) 350 | sign_input = "{subkey}\n{pubkey}\n{apitype}\n{params}".format( 351 | subkey=self.subscribe_key, 352 | pubkey=self.publish_key, 353 | apitype="audit" if (apicode) else "grant", 354 | params=params 355 | ) 356 | query['signature'] = self._pam_sign(sign_input) 357 | 358 | return self._request({"urlcomponents": [ 359 | 'v1', 'auth', "audit" if (apicode) else "grant", 360 | 'sub-key', 361 | self.subscribe_key 362 | ], 'urlparams': query}, 363 | self._return_wrapped_callback(callback), 364 | self._return_wrapped_callback(error)) 365 | 366 | def get_origin(self): 367 | return self.origin 368 | 369 | def set_auth_key(self, auth_key): 370 | self.auth_key = auth_key 371 | 372 | def get_auth_key(self): 373 | return self.auth_key 374 | 375 | def grant(self, channel=None, channel_group=None, auth_key=False, read=False, 376 | write=False, manage=False, ttl=5, callback=None, error=None): 377 | """Method for granting permissions. 378 | 379 | This function establishes subscribe and/or write permissions for 380 | PubNub Access Manager (PAM) by setting the read or write attribute 381 | to true. A grant with read or write set to false (or not included) 382 | will revoke any previous grants with read or write set to true. 383 | 384 | Permissions can be applied to any one of three levels: 385 | 1. Application level privileges are based on subscribe_key applying to all associated channels. 386 | 2. Channel level privileges are based on a combination of subscribe_key and channel name. 387 | 3. User level privileges are based on the combination of subscribe_key, channel and auth_key. 388 | 389 | Args: 390 | channel: (string) (optional) 391 | Specifies channel name to grant permissions to. 392 | If channel/channel_group is not specified, the grant applies to all 393 | channels associated with the subscribe_key. If auth_key 394 | is not specified, it is possible to grant permissions to 395 | multiple channels simultaneously by specifying the channels 396 | as a comma separated list. 397 | channel_group: (string) (optional) 398 | Specifies channel group name to grant permissions to. 399 | If channel/channel_group is not specified, the grant applies to all 400 | channels associated with the subscribe_key. If auth_key 401 | is not specified, it is possible to grant permissions to 402 | multiple channel groups simultaneously by specifying the channel groups 403 | as a comma separated list. 404 | 405 | auth_key: (string) (optional) 406 | Specifies auth_key to grant permissions to. 407 | It is possible to specify multiple auth_keys as comma 408 | separated list in combination with a single channel name. 409 | If auth_key is provided as the special-case value "null" 410 | (or included in a comma-separated list, eg. "null,null,abc"), 411 | a new auth_key will be generated and returned for each "null" value. 412 | 413 | read: (boolean) (default: True) 414 | Read permissions are granted by setting to True. 415 | Read permissions are removed by setting to False. 416 | 417 | write: (boolean) (default: True) 418 | Write permissions are granted by setting to true. 419 | Write permissions are removed by setting to false. 420 | manage: (boolean) (default: True) 421 | Manage permissions are granted by setting to true. 422 | Manage permissions are removed by setting to false. 423 | 424 | ttl: (int) (default: 1440 i.e 24 hrs) 425 | Time in minutes for which granted permissions are valid. 426 | Max is 525600 , Min is 1. 427 | Setting ttl to 0 will apply the grant indefinitely. 428 | 429 | callback: (function) (optional) 430 | A callback method can be passed to the method. 431 | If set, the api works in async mode. 432 | Required argument when working with twisted or tornado 433 | 434 | error: (function) (optional) 435 | An error method can be passed to the method. 436 | If set, the api works in async mode. 437 | Required argument when working with twisted or tornado . 438 | 439 | Returns: 440 | Returns a dict in sync mode i.e. when callback argument is not given 441 | The dict returned contains values with keys 'message' and 'payload' 442 | 443 | Sample Response: 444 | { 445 | "message":"Success", 446 | "payload":{ 447 | "ttl":5, 448 | "auths":{ 449 | "my_ro_authkey":{"r":1,"w":0} 450 | }, 451 | "subscribe_key":"my_subkey", 452 | "level":"user", 453 | "channel":"my_channel" 454 | } 455 | } 456 | """ 457 | 458 | return self._pam_auth({ 459 | 'channel' : channel, 460 | 'channel-group' : channel_group, 461 | 'auth' : auth_key, 462 | 'r' : read and 1 or 0, 463 | 'w' : write and 1 or 0, 464 | 'm' : manage and 1 or 0, 465 | 'ttl' : ttl, 466 | 'pnsdk' : self.pnsdk 467 | }, callback=callback, error=error) 468 | 469 | def revoke(self, channel=None, channel_group=None, auth_key=None, ttl=1, callback=None, error=None): 470 | """Method for revoking permissions. 471 | 472 | Args: 473 | channel: (string) (optional) 474 | Specifies channel name to revoke permissions to. 475 | If channel/channel_group is not specified, the revoke applies to all 476 | channels associated with the subscribe_key. If auth_key 477 | is not specified, it is possible to grant permissions to 478 | multiple channels simultaneously by specifying the channels 479 | as a comma separated list. 480 | 481 | channel_group: (string) (optional) 482 | Specifies channel group name to revoke permissions to. 483 | If channel/channel_group is not specified, the grant applies to all 484 | channels associated with the subscribe_key. If auth_key 485 | is not specified, it is possible to revoke permissions to 486 | multiple channel groups simultaneously by specifying the channel groups 487 | as a comma separated list. 488 | 489 | auth_key: (string) (optional) 490 | Specifies auth_key to revoke permissions to. 491 | It is possible to specify multiple auth_keys as comma 492 | separated list in combination with a single channel name. 493 | If auth_key is provided as the special-case value "null" 494 | (or included in a comma-separated list, eg. "null,null,abc"), 495 | a new auth_key will be generated and returned for each "null" value. 496 | 497 | ttl: (int) (default: 1440 i.e 24 hrs) 498 | Time in minutes for which granted permissions are valid. 499 | Max is 525600 , Min is 1. 500 | Setting ttl to 0 will apply the grant indefinitely. 501 | 502 | callback: (function) (optional) 503 | A callback method can be passed to the method. 504 | If set, the api works in async mode. 505 | Required argument when working with twisted or tornado 506 | 507 | error: (function) (optional) 508 | An error method can be passed to the method. 509 | If set, the api works in async mode. 510 | Required argument when working with twisted or tornado . 511 | 512 | Returns: 513 | Returns a dict in sync mode i.e. when callback argument is not given 514 | The dict returned contains values with keys 'message' and 'payload' 515 | 516 | Sample Response: 517 | { 518 | "message":"Success", 519 | "payload":{ 520 | "ttl":5, 521 | "auths":{ 522 | "my_authkey":{"r":0,"w":0} 523 | }, 524 | "subscribe_key":"my_subkey", 525 | "level":"user", 526 | "channel":"my_channel" 527 | } 528 | } 529 | 530 | """ 531 | 532 | return self._pam_auth({ 533 | 'channel' : channel, 534 | 'channel-group' : channel_group, 535 | 'auth' : auth_key, 536 | 'r' : 0, 537 | 'w' : 0, 538 | 'ttl' : ttl, 539 | 'pnsdk' : self.pnsdk 540 | }, callback=callback, error=error) 541 | 542 | def audit(self, channel=None, channel_group=None, auth_key=None, callback=None, error=None): 543 | """Method for fetching permissions from pubnub servers. 544 | 545 | This method provides a mechanism to reveal existing PubNub Access Manager attributes 546 | for any combination of subscribe_key, channel and auth_key. 547 | 548 | Args: 549 | channel: (string) (optional) 550 | Specifies channel name to return PAM 551 | attributes optionally in combination with auth_key. 552 | If channel/channel_group is not specified, results for all channels 553 | associated with subscribe_key are returned. 554 | If auth_key is not specified, it is possible to return 555 | results for a comma separated list of channels. 556 | channel_group: (string) (optional) 557 | Specifies channel group name to return PAM 558 | attributes optionally in combination with auth_key. 559 | If channel/channel_group is not specified, results for all channels 560 | associated with subscribe_key are returned. 561 | If auth_key is not specified, it is possible to return 562 | results for a comma separated list of channels. 563 | 564 | auth_key: (string) (optional) 565 | Specifies the auth_key to return PAM attributes for. 566 | If only a single channel is specified, it is possible to return 567 | results for a comma separated list of auth_keys. 568 | 569 | callback: (function) (optional) 570 | A callback method can be passed to the method. 571 | If set, the api works in async mode. 572 | Required argument when working with twisted or tornado 573 | 574 | error: (function) (optional) 575 | An error method can be passed to the method. 576 | If set, the api works in async mode. 577 | Required argument when working with twisted or tornado . 578 | 579 | Returns: 580 | Returns a dict in sync mode i.e. when callback argument is not given 581 | The dict returned contains values with keys 'message' and 'payload' 582 | 583 | Sample Response 584 | { 585 | "message":"Success", 586 | "payload":{ 587 | "channels":{ 588 | "my_channel":{ 589 | "auths":{"my_ro_authkey":{"r":1,"w":0}, 590 | "my_rw_authkey":{"r":0,"w":1}, 591 | "my_admin_authkey":{"r":1,"w":1} 592 | } 593 | } 594 | }, 595 | } 596 | 597 | Usage: 598 | 599 | pubnub.audit ('my_channel'); # Sync Mode 600 | 601 | """ 602 | 603 | return self._pam_auth({ 604 | 'channel' : channel, 605 | 'channel-group' : channel_group, 606 | 'auth' : auth_key, 607 | 'pnsdk' : self.pnsdk 608 | }, 1, callback=callback, error=error) 609 | 610 | def encrypt(self, message): 611 | """Method for encrypting data. 612 | 613 | This method takes plaintext as input and returns encrypted data. 614 | This need not be called directly as enncryption/decryption is 615 | taken care of transparently by Pubnub class if cipher key is 616 | provided at time of initializing pubnub object 617 | 618 | Args: 619 | message: Message to be encrypted. 620 | 621 | Returns: 622 | Returns encrypted message if cipher key is set 623 | """ 624 | if self.cipher_key: 625 | message = json.dumps(self.pc.encrypt( 626 | self.cipher_key, json.dumps(message)).replace('\n', '')) 627 | else: 628 | message = json.dumps(message) 629 | 630 | return message 631 | 632 | def decrypt(self, message): 633 | """Method for decrypting data. 634 | 635 | This method takes ciphertext as input and returns decrypted data. 636 | This need not be called directly as enncryption/decryption is 637 | taken care of transparently by Pubnub class if cipher key is 638 | provided at time of initializing pubnub object 639 | 640 | Args: 641 | message: Message to be decrypted. 642 | 643 | Returns: 644 | Returns decrypted message if cipher key is set 645 | """ 646 | if self.cipher_key: 647 | message = self.pc.decrypt(self.cipher_key, message) 648 | 649 | return message 650 | 651 | def _return_wrapped_callback(self, callback=None): 652 | def _new_format_callback(response): 653 | if 'payload' in response: 654 | if (callback is not None): 655 | callback_data = dict() 656 | callback_data['payload'] = response['payload'] 657 | 658 | if 'message' in response: 659 | callback_data['message'] = response['message'] 660 | 661 | callback(callback_data) 662 | else: 663 | if (callback is not None): 664 | callback(response) 665 | if (callback is not None): 666 | return _new_format_callback 667 | else: 668 | return None 669 | 670 | def leave_channel(self, channel, callback=None, error=None): 671 | ## Send leave 672 | return self._request({"urlcomponents": [ 673 | 'v2', 'presence', 674 | 'sub_key', 675 | self.subscribe_key, 676 | 'channel', 677 | channel, 678 | 'leave' 679 | ], 'urlparams': {'auth': self.auth_key, 'pnsdk' : self.pnsdk, "uuid": self.uuid,}}, 680 | callback=self._return_wrapped_callback(callback), 681 | error=self._return_wrapped_callback(error)) 682 | 683 | def leave_group(self, channel_group, callback=None, error=None): 684 | ## Send leave 685 | return self._request({"urlcomponents": [ 686 | 'v2', 'presence', 687 | 'sub_key', 688 | self.subscribe_key, 689 | 'channel', 690 | ',', 691 | 'leave' 692 | ], 'urlparams': {'auth': self.auth_key, 'pnsdk' : self.pnsdk, 'channel-group' : channel_group, "uuid": self.uuid,}}, 693 | callback=self._return_wrapped_callback(callback), 694 | error=self._return_wrapped_callback(error)) 695 | 696 | 697 | def publish(self, channel, message, callback=None, error=None): 698 | """Publishes data on a channel. 699 | 700 | The publish() method is used to send a message to all subscribers of a channel. 701 | To publish a message you must first specify a valid publish_key at initialization. 702 | A successfully published message is replicated across the PubNub Real-Time Network 703 | and sent simultaneously to all subscribed clients on a channel. 704 | Messages in transit can be secured from potential eavesdroppers with SSL/TLS by 705 | setting ssl to True during initialization. 706 | 707 | Published messages can also be encrypted with AES-256 simply by specifying a cipher_key 708 | during initialization. 709 | 710 | Args: 711 | channel: (string) 712 | Specifies channel name to publish messages to. 713 | message: (string/int/double/dict/list) 714 | Message to be published 715 | callback: (optional) 716 | A callback method can be passed to the method. 717 | If set, the api works in async mode. 718 | Required argument when working with twisted or tornado 719 | error: (optional) 720 | An error method can be passed to the method. 721 | If set, the api works in async mode. 722 | Required argument when working with twisted or tornado 723 | 724 | Returns: 725 | Sync Mode : list 726 | Async Mode : None 727 | 728 | The function returns the following formatted response: 729 | 730 | [ Number, "Status", "Time Token"] 731 | 732 | The output below demonstrates the response to a successful call: 733 | 734 | [1,"Sent","13769558699541401"] 735 | 736 | """ 737 | 738 | message = self.encrypt(message) 739 | 740 | ## Send Message 741 | return self._request({"urlcomponents": [ 742 | 'publish', 743 | self.publish_key, 744 | self.subscribe_key, 745 | '0', 746 | channel, 747 | '0', 748 | message 749 | ], 'urlparams': {'auth': self.auth_key, 'pnsdk' : self.pnsdk}}, 750 | callback=self._return_wrapped_callback(callback), 751 | error=self._return_wrapped_callback(error)) 752 | 753 | def presence(self, channel, callback, error=None): 754 | """Subscribe to presence events on a channel. 755 | 756 | Only works in async mode 757 | 758 | Args: 759 | channel: Channel name ( string ) on which to listen for events 760 | callback: A callback method should be passed as parameter. 761 | If passed, the api works in async mode. 762 | Required argument when working with twisted or tornado . 763 | error: Optional variable. An error method can be passed as parameter. 764 | If set, the api works in async mode. 765 | 766 | Returns: 767 | None 768 | """ 769 | return self.subscribe(channel+'-pnpres', callback=callback) 770 | 771 | def presence_group(self, channel_group, callback, error=None): 772 | """Subscribe to presence events on a channel group. 773 | 774 | Only works in async mode 775 | 776 | Args: 777 | channel_group: Channel group name ( string ) 778 | callback: A callback method should be passed to the method. 779 | If passed, the api works in async mode. 780 | Required argument when working with twisted or tornado . 781 | error: Optional variable. An error method can be passed as parameter. 782 | If passed, the api works in async mode. 783 | 784 | Returns: 785 | None 786 | """ 787 | return self.subscribe_group(channel_group+'-pnpres', callback=callback) 788 | 789 | def here_now(self, channel, callback=None, error=None): 790 | """Get here now data. 791 | 792 | You can obtain information about the current state of a channel including 793 | a list of unique user-ids currently subscribed to the channel and the total 794 | occupancy count of the channel by calling the here_now() function in your 795 | application. 796 | 797 | 798 | Args: 799 | channel: (string) (optional) 800 | Specifies the channel name to return occupancy results. 801 | If channel is not provided, here_now will return data for all channels. 802 | 803 | callback: (optional) 804 | A callback method should be passed to the method. 805 | If set, the api works in async mode. 806 | Required argument when working with twisted or tornado . 807 | 808 | error: (optional) 809 | Optional variable. An error method can be passed to the method. 810 | If set, the api works in async mode. 811 | Required argument when working with twisted or tornado . 812 | 813 | Returns: 814 | Sync Mode: list 815 | Async Mode: None 816 | 817 | Response Format: 818 | 819 | The here_now() method returns a list of uuid s currently subscribed to the channel. 820 | 821 | uuids:["String","String", ... ,"String"] - List of UUIDs currently subscribed to the channel. 822 | 823 | occupancy: Number - Total current occupancy of the channel. 824 | 825 | Example Response: 826 | { 827 | occupancy: 4, 828 | uuids: [ 829 | '123123234t234f34fq3dq', 830 | '143r34f34t34fq34q34q3', 831 | '23f34d3f4rq34r34rq23q', 832 | 'w34tcw45t45tcw435tww3', 833 | ] 834 | } 835 | """ 836 | 837 | urlcomponents = [ 838 | 'v2', 'presence', 839 | 'sub_key', self.subscribe_key 840 | ] 841 | 842 | if (channel is not None and len(channel) > 0): 843 | urlcomponents.append('channel') 844 | urlcomponents.append(channel) 845 | 846 | ## Get Presence Here Now 847 | return self._request({"urlcomponents": urlcomponents, 848 | 'urlparams': {'auth': self.auth_key, 'pnsdk' : self.pnsdk}}, 849 | callback=self._return_wrapped_callback(callback), 850 | error=self._return_wrapped_callback(error)) 851 | 852 | 853 | def history(self, channel, count=100, reverse=False, 854 | start=None, end=None, callback=None, error=None): 855 | """This method fetches historical messages of a channel. 856 | 857 | PubNub Storage/Playback Service provides real-time access to an unlimited 858 | history for all messages published to PubNub. Stored messages are replicated 859 | across multiple availability zones in several geographical data center 860 | locations. Stored messages can be encrypted with AES-256 message encryption 861 | ensuring that they are not readable while stored on PubNub's network. 862 | 863 | It is possible to control how messages are returned and in what order, 864 | for example you can: 865 | 866 | Return messages in the order newest to oldest (default behavior). 867 | 868 | Return messages in the order oldest to newest by setting reverse to true. 869 | 870 | Page through results by providing a start or end time token. 871 | 872 | Retrieve a "slice" of the time line by providing both a start and end time token. 873 | 874 | Limit the number of messages to a specific quantity using the count parameter. 875 | 876 | 877 | 878 | Args: 879 | channel: (string) 880 | Specifies channel to return history messages from 881 | 882 | count: (int) (default: 100) 883 | Specifies the number of historical messages to return 884 | 885 | callback: (optional) 886 | A callback method should be passed to the method. 887 | If set, the api works in async mode. 888 | Required argument when working with twisted or tornado . 889 | 890 | error: (optional) 891 | An error method can be passed to the method. 892 | If set, the api works in async mode. 893 | Required argument when working with twisted or tornado . 894 | 895 | Returns: 896 | Returns a list in sync mode i.e. when callback argument is not given 897 | 898 | Sample Response: 899 | [["Pub1","Pub2","Pub3","Pub4","Pub5"],13406746729185766,13406746845892666] 900 | """ 901 | 902 | params = dict() 903 | 904 | params['count'] = count 905 | params['reverse'] = reverse 906 | params['start'] = start 907 | params['end'] = end 908 | params['auth_key'] = self.auth_key 909 | params['pnsdk'] = self.pnsdk 910 | 911 | ## Get History 912 | return self._request({'urlcomponents': [ 913 | 'v2', 914 | 'history', 915 | 'sub-key', 916 | self.subscribe_key, 917 | 'channel', 918 | channel, 919 | ], 'urlparams': params}, 920 | callback=self._return_wrapped_callback(callback), 921 | error=self._return_wrapped_callback(error)) 922 | 923 | def time(self, callback=None): 924 | """This function will return a 17 digit precision Unix epoch. 925 | 926 | Args: 927 | 928 | callback: (optional) 929 | A callback method should be passed to the method. 930 | If set, the api works in async mode. 931 | Required argument when working with twisted or tornado . 932 | 933 | Returns: 934 | Returns a 17 digit number in sync mode i.e. when callback argument is not given 935 | 936 | Sample: 937 | 13769501243685161 938 | """ 939 | 940 | time = self._request({'urlcomponents': [ 941 | 'time', 942 | '0' 943 | ]}, callback) 944 | if time is not None: 945 | return time[0] 946 | 947 | def _encode(self, request): 948 | return [ 949 | "".join([' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?'.find(ch) > -1 and 950 | hex(ord(ch)).replace('0x', '%').upper() or 951 | ch for ch in list(bit) 952 | ]) for bit in request] 953 | 954 | def getUrl(self, request): 955 | 956 | if self.u is True and "urlparams" in request: 957 | request['urlparams']['u'] = str(random.randint(1, 100000000000)) 958 | ## Build URL 959 | url = self.origin + '/' + "/".join([ 960 | "".join([' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?'.find(ch) > -1 and 961 | hex(ord(ch)).replace('0x', '%').upper() or 962 | ch for ch in list(bit) 963 | ]) for bit in request["urlcomponents"]]) 964 | if ("urlparams" in request): 965 | url = url + '?' + "&".join([x + "=" + str(y) for x, y in request[ 966 | "urlparams"].items() if y is not None and len(str(y)) > 0]) 967 | 968 | return url 969 | 970 | def _channel_registry(self, url=None, params=None, callback=None, error=None): 971 | 972 | if (params is None): 973 | params = dict() 974 | 975 | urlcomponents = ['v1', 'channel-registration', 'sub-key', self.subscribe_key ] 976 | 977 | if (url is not None): 978 | urlcomponents += url 979 | 980 | params['auth'] = self.auth_key 981 | params['pnsdk'] = self.pnsdk 982 | 983 | ## Get History 984 | return self._request({'urlcomponents': urlcomponents, 'urlparams': params}, 985 | callback=self._return_wrapped_callback(callback), 986 | error=self._return_wrapped_callback(error)) 987 | 988 | def _channel_group(self, channel_group=None, channels=None, cloak=None,mode='add', callback=None, error=None): 989 | params = dict() 990 | url = [] 991 | namespace = None 992 | 993 | if (channel_group is not None and len(channel_group) > 0): 994 | ns_ch_a = channel_group.split(':') 995 | 996 | if len(ns_ch_a) > 1: 997 | namespace = None if ns_ch_a[0] == '*' else ns_ch_a[0] 998 | channel_group = ns_ch_a[1] 999 | else: 1000 | channel_group = ns_ch_a[0] 1001 | 1002 | if (namespace is not None): 1003 | url.append('namespace') 1004 | url.append(self._encode(namespace)) 1005 | 1006 | url.append('channel-group') 1007 | 1008 | if channel_group is not None and channel_group != '*': 1009 | url.append(channel_group) 1010 | 1011 | if (channels is not None): 1012 | if (type(channels) is list): 1013 | channels = ','.join(channels) 1014 | params[mode] = channels 1015 | #params['cloak'] = 'true' if CLOAK is True else 'false' 1016 | else: 1017 | if mode == 'remove': 1018 | url.append('remove') 1019 | 1020 | return self._channel_registry(url=url, params=params, callback=callback, error=error) 1021 | 1022 | 1023 | def channel_group_list_namespaces(self, callback=None, error=None): 1024 | """Get list of namespaces. 1025 | 1026 | You can obtain list of namespaces for the subscribe key associated with PubNub 1027 | object using this method. 1028 | 1029 | 1030 | Args: 1031 | callback: (optional) 1032 | A callback method should be passed to the method. 1033 | If set, the api works in async mode. 1034 | Required argument when working with twisted or tornado. 1035 | 1036 | error: (optional) 1037 | Optional variable. An error method can be passed to the method. 1038 | If set, the api works in async mode. 1039 | Required argument when working with twisted or tornado. 1040 | 1041 | Returns: 1042 | Sync Mode: dict 1043 | channel_group_list_namespaces method returns a dict which contains list of namespaces 1044 | in payload field 1045 | { 1046 | u'status': 200, 1047 | u'payload': { 1048 | u'sub_key': u'demo', 1049 | u'namespaces': [u'dev', u'foo'] 1050 | }, 1051 | u'service': u'channel-registry', 1052 | u'error': False 1053 | } 1054 | 1055 | Async Mode: None (callback gets the response as parameter) 1056 | 1057 | Response Format: 1058 | 1059 | The callback passed to channel_group_list_namespaces gets the a dict containing list of namespaces 1060 | under payload field 1061 | 1062 | { 1063 | u'payload': { 1064 | u'sub_key': u'demo', 1065 | u'namespaces': [u'dev', u'foo'] 1066 | } 1067 | } 1068 | 1069 | namespaces is the list of namespaces for the given subscribe key 1070 | 1071 | 1072 | """ 1073 | 1074 | url = ['namespace'] 1075 | return self._channel_registry(url=url, callback=callback, error=error) 1076 | 1077 | def channel_group_remove_namespace(self, namespace, callback=None, error=None): 1078 | """Remove a namespace. 1079 | 1080 | A namespace can be deleted using this method. 1081 | 1082 | 1083 | Args: 1084 | namespace: (string) namespace to be deleted 1085 | callback: (optional) 1086 | A callback method should be passed to the method. 1087 | If set, the api works in async mode. 1088 | Required argument when working with twisted or tornado . 1089 | 1090 | error: (optional) 1091 | Optional variable. An error method can be passed to the method. 1092 | If set, the api works in async mode. 1093 | Required argument when working with twisted or tornado . 1094 | 1095 | Returns: 1096 | Sync Mode: dict 1097 | channel_group_remove_namespace method returns a dict indicating status of the request 1098 | 1099 | { 1100 | u'status': 200, 1101 | u'message': 'OK', 1102 | u'service': u'channel-registry', 1103 | u'error': False 1104 | } 1105 | 1106 | Async Mode: None ( callback gets the response as parameter ) 1107 | 1108 | Response Format: 1109 | 1110 | The callback passed to channel_group_list_namespaces gets the a dict indicating status of the request 1111 | 1112 | { 1113 | u'status': 200, 1114 | u'message': 'OK', 1115 | u'service': u'channel-registry', 1116 | u'error': False 1117 | } 1118 | 1119 | """ 1120 | url = ['namespace', self._encode(namespace), 'remove'] 1121 | return self._channel_registry(url=url, callback=callback, error=error) 1122 | 1123 | def channel_group_list_groups(self, namespace=None, callback=None, error=None): 1124 | """Get list of groups. 1125 | 1126 | Using this method, list of groups for the subscribe key associated with PubNub 1127 | object, can be obtained. If namespace is provided, groups within the namespace 1128 | only are listed 1129 | 1130 | Args: 1131 | namespace: (string) (optional) namespace 1132 | callback: (optional) 1133 | A callback method should be passed to the method. 1134 | If set, the api works in async mode. 1135 | Required argument when working with twisted or tornado . 1136 | 1137 | error: (optional) 1138 | Optional variable. An error method can be passed to the method. 1139 | If set, the api works in async mode. 1140 | Required argument when working with twisted or tornado . 1141 | 1142 | Returns: 1143 | Sync Mode: dict 1144 | channel_group_list_groups method returns a dict which contains list of groups 1145 | in payload field 1146 | { 1147 | u'status': 200, 1148 | u'payload': {"namespace": "dev", "groups": ["abcd"]}, 1149 | u'service': u'channel-registry', 1150 | u'error': False 1151 | } 1152 | 1153 | Async Mode: None ( callback gets the response as parameter ) 1154 | 1155 | Response Format: 1156 | 1157 | The callback passed to channel_group_list_namespaces gets the a dict containing list of groups 1158 | under payload field 1159 | 1160 | { 1161 | u'payload': {"namespace": "dev", "groups": ["abcd"]} 1162 | } 1163 | 1164 | 1165 | 1166 | """ 1167 | 1168 | if (namespace is not None and len(namespace) > 0): 1169 | channel_group = namespace + ':*' 1170 | else: 1171 | channel_group = '*:*' 1172 | 1173 | return self._channel_group(channel_group=channel_group, callback=callback, error=error) 1174 | 1175 | def channel_group_list_channels(self, channel_group, callback=None, error=None): 1176 | """Get list of channels for a group. 1177 | 1178 | Using this method, list of channels for a group, can be obtained. 1179 | 1180 | Args: 1181 | channel_group: (string) (optional) 1182 | Channel Group name. It can also contain namespace. 1183 | If namespace is also specified, then the parameter 1184 | will be in format namespace:channel_group 1185 | 1186 | callback: (optional) 1187 | A callback method should be passed to the method. 1188 | If set, the api works in async mode. 1189 | Required argument when working with twisted or tornado. 1190 | 1191 | error: (optional) 1192 | Optional variable. An error method can be passed to the method. 1193 | If set, the api works in async mode. 1194 | Required argument when working with twisted or tornado. 1195 | 1196 | Returns: 1197 | Sync Mode: dict 1198 | channel_group_list_channels method returns a dict which contains list of channels 1199 | in payload field 1200 | { 1201 | u'status': 200, 1202 | u'payload': {"channels": ["hi"], "group": "abcd"}, 1203 | u'service': u'channel-registry', 1204 | u'error': False 1205 | } 1206 | 1207 | Async Mode: None ( callback gets the response as parameter ) 1208 | 1209 | Response Format: 1210 | 1211 | The callback passed to channel_group_list_channels gets the a dict containing list of channels 1212 | under payload field 1213 | 1214 | { 1215 | u'payload': {"channels": ["hi"], "group": "abcd"} 1216 | } 1217 | 1218 | 1219 | """ 1220 | return self._channel_group(channel_group=channel_group, callback=callback, error=error) 1221 | 1222 | def channel_group_add_channel(self, channel_group, channel, callback=None, error=None): 1223 | """Add a channel to group. 1224 | 1225 | A channel can be added to group using this method. 1226 | 1227 | 1228 | Args: 1229 | channel_group: (string) 1230 | Channel Group name. It can also contain namespace. 1231 | If namespace is also specified, then the parameter 1232 | will be in format namespace:channel_group 1233 | channel: (string) 1234 | Can be a channel name, a list of channel names, 1235 | or a comma separated list of channel names 1236 | callback: (optional) 1237 | A callback method should be passed to the method. 1238 | If set, the api works in async mode. 1239 | Required argument when working with twisted or tornado. 1240 | 1241 | error: (optional) 1242 | Optional variable. An error method can be passed to the method. 1243 | If set, the api works in async mode. 1244 | Required argument when working with twisted or tornado. 1245 | 1246 | Returns: 1247 | Sync Mode: dict 1248 | channel_group_add_channel method returns a dict indicating status of the request 1249 | 1250 | { 1251 | u'status': 200, 1252 | u'message': 'OK', 1253 | u'service': u'channel-registry', 1254 | u'error': False 1255 | } 1256 | 1257 | Async Mode: None ( callback gets the response as parameter ) 1258 | 1259 | Response Format: 1260 | 1261 | The callback passed to channel_group_add_channel gets the a dict indicating status of the request 1262 | 1263 | { 1264 | u'status': 200, 1265 | u'message': 'OK', 1266 | u'service': u'channel-registry', 1267 | u'error': False 1268 | } 1269 | 1270 | """ 1271 | 1272 | return self._channel_group(channel_group=channel_group, channels=channel, mode='add', callback=callback, error=error) 1273 | 1274 | def channel_group_remove_channel(self, channel_group, channel, callback=None, error=None): 1275 | """Remove channel. 1276 | 1277 | A channel can be removed from a group method. 1278 | 1279 | 1280 | Args: 1281 | channel_group: (string) 1282 | Channel Group name. It can also contain namespace. 1283 | If namespace is also specified, then the parameter 1284 | will be in format namespace:channel_group 1285 | channel: (string) 1286 | Can be a channel name, a list of channel names, 1287 | or a comma separated list of channel names 1288 | callback: (optional) 1289 | A callback method should be passed to the method. 1290 | If set, the api works in async mode. 1291 | Required argument when working with twisted or tornado . 1292 | 1293 | error: (optional) 1294 | Optional variable. An error method can be passed to the method. 1295 | If set, the api works in async mode. 1296 | Required argument when working with twisted or tornado . 1297 | 1298 | Returns: 1299 | Sync Mode: dict 1300 | channel_group_remove_channel method returns a dict indicating status of the request 1301 | 1302 | { 1303 | u'status': 200, 1304 | u'message': 'OK', 1305 | u'service': u'channel-registry', 1306 | u'error': False 1307 | } 1308 | 1309 | Async Mode: None ( callback gets the response as parameter ) 1310 | 1311 | Response Format: 1312 | 1313 | The callback passed to channel_group_remove_channel gets the a dict indicating status of the request 1314 | 1315 | { 1316 | u'status': 200, 1317 | u'message': 'OK', 1318 | u'service': u'channel-registry', 1319 | u'error': False 1320 | } 1321 | 1322 | """ 1323 | 1324 | return self._channel_group(channel_group=channel_group, channels=channel, mode='remove', callback=callback, error=error) 1325 | 1326 | def channel_group_remove_group(self, channel_group, callback=None, error=None): 1327 | """Remove channel group. 1328 | 1329 | A channel group can be removed using this method. 1330 | 1331 | 1332 | Args: 1333 | channel_group: (string) 1334 | Channel Group name. It can also contain namespace. 1335 | If namespace is also specified, then the parameter 1336 | will be in format namespace:channel_group 1337 | callback: (optional) 1338 | A callback method should be passed to the method. 1339 | If set, the api works in async mode. 1340 | Required argument when working with twisted or tornado. 1341 | 1342 | error: (optional) 1343 | Optional variable. An error method can be passed to the method. 1344 | If set, the api works in async mode. 1345 | Required argument when working with twisted or tornado. 1346 | 1347 | Returns: 1348 | Sync Mode: dict 1349 | channel_group_remove_group method returns a dict indicating status of the request 1350 | 1351 | { 1352 | u'status': 200, 1353 | u'message': 'OK', 1354 | u'service': u'channel-registry', 1355 | u'error': False 1356 | } 1357 | 1358 | Async Mode: None ( callback gets the response as parameter ) 1359 | 1360 | Response Format: 1361 | 1362 | The callback passed to channel_group_remove_group gets the a dict indicating status of the request 1363 | 1364 | { 1365 | u'status': 200, 1366 | u'message': 'OK', 1367 | u'service': u'channel-registry', 1368 | u'error': False 1369 | } 1370 | 1371 | """ 1372 | 1373 | return self._channel_group(channel_group=channel_group, mode='remove', callback=callback, error=error) 1374 | 1375 | 1376 | 1377 | class EmptyLock(): 1378 | def __enter__(self): 1379 | pass 1380 | 1381 | def __exit__(self, a, b, c): 1382 | pass 1383 | 1384 | empty_lock = EmptyLock() 1385 | 1386 | 1387 | class PubnubCoreAsync(PubnubBase): 1388 | 1389 | def start(self): 1390 | pass 1391 | 1392 | def stop(self): 1393 | pass 1394 | 1395 | def __init__( 1396 | self, 1397 | publish_key, 1398 | subscribe_key, 1399 | secret_key=None, 1400 | cipher_key=None, 1401 | auth_key=None, 1402 | ssl_on=False, 1403 | origin='pubsub.pubnub.com', 1404 | uuid=None, 1405 | _tt_lock=empty_lock, 1406 | _channel_list_lock=empty_lock, 1407 | _channel_group_list_lock=empty_lock 1408 | ): 1409 | 1410 | super(PubnubCoreAsync, self).__init__( 1411 | publish_key=publish_key, 1412 | subscribe_key=subscribe_key, 1413 | secret_key=secret_key, 1414 | cipher_key=cipher_key, 1415 | auth_key=auth_key, 1416 | ssl_on=ssl_on, 1417 | origin=origin, 1418 | uuid=uuid 1419 | ) 1420 | 1421 | self.subscriptions = {} 1422 | self.subscription_groups = {} 1423 | self.timetoken = 0 1424 | self.last_timetoken = 0 1425 | self.accept_encoding = 'gzip' 1426 | self.SUB_RECEIVER = None 1427 | self._connect = None 1428 | self._tt_lock = _tt_lock 1429 | self._channel_list_lock = _channel_list_lock 1430 | self._channel_group_list_lock = _channel_group_list_lock 1431 | self._connect = lambda: None 1432 | self.u = None 1433 | 1434 | def get_channel_list(self, channels): 1435 | channel = '' 1436 | first = True 1437 | with self._channel_list_lock: 1438 | for ch in channels: 1439 | if not channels[ch]['subscribed']: 1440 | continue 1441 | if not first: 1442 | channel += ',' 1443 | else: 1444 | first = False 1445 | channel += ch 1446 | return channel 1447 | 1448 | def get_channel_group_list(self, channel_groups): 1449 | channel_group = '' 1450 | first = True 1451 | with self._channel_group_list_lock: 1452 | for ch in channel_groups: 1453 | if not channel_groups[ch]['subscribed']: 1454 | continue 1455 | if not first: 1456 | channel_group += ',' 1457 | else: 1458 | first = False 1459 | channel_group += ch 1460 | return channel_group 1461 | 1462 | 1463 | def get_channel_array(self): 1464 | """Get List of currently subscribed channels 1465 | 1466 | Returns: 1467 | Returns a list containing names of channels subscribed 1468 | 1469 | Sample return value: 1470 | ["a","b","c] 1471 | """ 1472 | channels = self.subscriptions 1473 | channel = [] 1474 | with self._channel_list_lock: 1475 | for ch in channels: 1476 | if not channels[ch]['subscribed']: 1477 | continue 1478 | channel.append(ch) 1479 | return channel 1480 | 1481 | def get_channel_group_array(self): 1482 | """Get List of currently subscribed channel groups 1483 | 1484 | Returns: 1485 | Returns a list containing names of channel groups subscribed 1486 | 1487 | Sample return value: 1488 | ["a","b","c] 1489 | """ 1490 | channel_groups = self.subscription_groups 1491 | channel_group = [] 1492 | with self._channel_group_list_lock: 1493 | for ch in channel_groups: 1494 | if not channel_groups[ch]['subscribed']: 1495 | continue 1496 | channel_group.append(ch) 1497 | return channel_group 1498 | 1499 | def each(l, func): 1500 | if func is None: 1501 | return 1502 | for i in l: 1503 | func(i) 1504 | 1505 | def subscribe(self, channels, callback, error=None, 1506 | connect=None, disconnect=None, reconnect=None, sync=False): 1507 | """Subscribe to data on a channel. 1508 | 1509 | This function causes the client to create an open TCP socket to the 1510 | PubNub Real-Time Network and begin listening for messages on a specified channel. 1511 | To subscribe to a channel the client must send the appropriate subscribe_key at 1512 | initialization. 1513 | 1514 | Only works in async mode 1515 | 1516 | Args: 1517 | channel: (string/list) 1518 | Specifies the channel to subscribe to. It is possible to specify 1519 | multiple channels as a comma separated list or andarray. 1520 | 1521 | callback: (function) 1522 | This callback is called on receiving a message from the channel. 1523 | 1524 | error: (function) (optional) 1525 | This callback is called on an error event 1526 | 1527 | connect: (function) (optional) 1528 | This callback is called on a successful connection to the PubNub cloud 1529 | 1530 | disconnect: (function) (optional) 1531 | This callback is called on client disconnect from the PubNub cloud 1532 | 1533 | reconnect: (function) (optional) 1534 | This callback is called on successfully re-connecting to the PubNub cloud 1535 | 1536 | Returns: 1537 | None 1538 | """ 1539 | 1540 | return self._subscribe(channels=channels, callback=callback, error=error, 1541 | connect=connect, disconnect=disconnect, reconnect=reconnect, sync=sync) 1542 | 1543 | def subscribe_group(self, channel_groups, callback, error=None, 1544 | connect=None, disconnect=None, reconnect=None, sync=False): 1545 | """Subscribe to data on a channel group. 1546 | 1547 | This function causes the client to create an open TCP socket to the 1548 | PubNub Real-Time Network and begin listening for messages on a specified channel. 1549 | To subscribe to a channel group the client must send the appropriate subscribe_key at 1550 | initialization. 1551 | 1552 | Only works in async mode 1553 | 1554 | Args: 1555 | channel_groups: (string/list) 1556 | Specifies the channel groups to subscribe to. It is possible to specify 1557 | multiple channel groups as a comma separated list or andarray. 1558 | 1559 | callback: (function) 1560 | This callback is called on receiving a message from the channel. 1561 | 1562 | error: (function) (optional) 1563 | This callback is called on an error event 1564 | 1565 | connect: (function) (optional) 1566 | This callback is called on a successful connection to the PubNub cloud 1567 | 1568 | disconnect: (function) (optional) 1569 | This callback is called on client disconnect from the PubNub cloud 1570 | 1571 | reconnect: (function) (optional) 1572 | This callback is called on successfully re-connecting to the PubNub cloud 1573 | 1574 | Returns: 1575 | None 1576 | """ 1577 | 1578 | return self._subscribe(channel_groups=channel_groups, callback=callback, error=error, 1579 | connect=connect, disconnect=disconnect, reconnect=reconnect, sync=sync) 1580 | 1581 | def _subscribe(self, channels=None, channel_groups=None, callback=None, error=None, 1582 | connect=None, disconnect=None, reconnect=None, sync=False): 1583 | 1584 | with self._tt_lock: 1585 | self.last_timetoken = self.timetoken if self.timetoken != 0 \ 1586 | else self.last_timetoken 1587 | self.timetoken = 0 1588 | 1589 | if sync is True and self.subscribe_sync is not None: 1590 | self.subscribe_sync(args) 1591 | return 1592 | 1593 | def _invoke(func, msg=None, channel=None, real_channel=None): 1594 | if func is not None: 1595 | if msg is not None and channel is not None and real_channel is not None: 1596 | try: 1597 | func(get_data_for_user(msg), channel, real_channel) 1598 | except: 1599 | func(get_data_for_user(msg), channel) 1600 | elif msg is not None and channel is not None: 1601 | func(get_data_for_user(msg), channel) 1602 | elif msg is not None: 1603 | func(get_data_for_user(msg)) 1604 | else: 1605 | func() 1606 | 1607 | def _invoke_connect(): 1608 | if self._channel_list_lock: 1609 | with self._channel_list_lock: 1610 | for ch in self.subscriptions: 1611 | chobj = self.subscriptions[ch] 1612 | if chobj['connected'] is False: 1613 | chobj['connected'] = True 1614 | chobj['disconnected'] = False 1615 | _invoke(chobj['connect'], chobj['name']) 1616 | else: 1617 | if chobj['disconnected'] is True: 1618 | chobj['disconnected'] = False 1619 | _invoke(chobj['reconnect'], chobj['name']) 1620 | 1621 | if self._channel_group_list_lock: 1622 | with self._channel_group_list_lock: 1623 | for ch in self.subscription_groups: 1624 | chobj = self.subscription_groups[ch] 1625 | if chobj['connected'] is False: 1626 | chobj['connected'] = True 1627 | chobj['disconnected'] = False 1628 | _invoke(chobj['connect'], chobj['name']) 1629 | else: 1630 | if chobj['disconnected'] is True: 1631 | chobj['disconnected'] = False 1632 | _invoke(chobj['reconnect'], chobj['name']) 1633 | 1634 | 1635 | def _invoke_disconnect(): 1636 | if self._channel_list_lock: 1637 | with self._channel_list_lock: 1638 | for ch in self.subscriptions: 1639 | chobj = self.subscriptions[ch] 1640 | if chobj['connected'] is True: 1641 | if chobj['disconnected'] is False: 1642 | chobj['disconnected'] = True 1643 | _invoke(chobj['disconnect'], chobj['name']) 1644 | if self._channel_group_list_lock: 1645 | with self._channel_group_list_lock: 1646 | for ch in self.subscription_groups: 1647 | chobj = self.subscription_groups[ch] 1648 | if chobj['connected'] is True: 1649 | if chobj['disconnected'] is False: 1650 | chobj['disconnected'] = True 1651 | _invoke(chobj['disconnect'], chobj['name']) 1652 | 1653 | 1654 | def _invoke_error(channel_list=None, error=None): 1655 | if channel_list is None: 1656 | for ch in self.subscriptions: 1657 | chobj = self.subscriptions[ch] 1658 | _invoke(chobj['error'], error) 1659 | else: 1660 | for ch in channel_list: 1661 | chobj = self.subscriptions[ch] 1662 | _invoke(chobj['error'], error) 1663 | 1664 | def _get_channel(): 1665 | for ch in self.subscriptions: 1666 | chobj = self.subscriptions[ch] 1667 | if chobj['subscribed'] is True: 1668 | return chobj 1669 | 1670 | if channels is not None: 1671 | channels = channels if isinstance( 1672 | channels, list) else channels.split(",") 1673 | for channel in channels: 1674 | ## New Channel? 1675 | if len(channel) > 0 and \ 1676 | (not channel in self.subscriptions or 1677 | self.subscriptions[channel]['subscribed'] is False): 1678 | with self._channel_list_lock: 1679 | self.subscriptions[channel] = { 1680 | 'name': channel, 1681 | 'first': False, 1682 | 'connected': False, 1683 | 'disconnected': True, 1684 | 'subscribed': True, 1685 | 'callback': callback, 1686 | 'connect': connect, 1687 | 'disconnect': disconnect, 1688 | 'reconnect': reconnect, 1689 | 'error': error 1690 | } 1691 | 1692 | if channel_groups is not None: 1693 | channel_groups = channel_groups if isinstance( 1694 | channel_groups, list) else channel_groups.split(",") 1695 | 1696 | for channel_group in channel_groups: 1697 | ## New Channel? 1698 | if len(channel_group) > 0 and \ 1699 | (not channel_group in self.subscription_groups or 1700 | self.subscription_groups[channel_group]['subscribed'] is False): 1701 | with self._channel_group_list_lock: 1702 | self.subscription_groups[channel_group] = { 1703 | 'name': channel_group, 1704 | 'first': False, 1705 | 'connected': False, 1706 | 'disconnected': True, 1707 | 'subscribed': True, 1708 | 'callback': callback, 1709 | 'connect': connect, 1710 | 'disconnect': disconnect, 1711 | 'reconnect': reconnect, 1712 | 'error': error 1713 | } 1714 | 1715 | ''' 1716 | ## return if already connected to channel 1717 | if channel in self.subscriptions and \ 1718 | 'connected' in self.subscriptions[channel] and \ 1719 | self.subscriptions[channel]['connected'] is True: 1720 | _invoke(error, "Already Connected") 1721 | return 1722 | ''' 1723 | ## SUBSCRIPTION RECURSION 1724 | def _connect(): 1725 | 1726 | self._reset_offline() 1727 | 1728 | def error_callback(response): 1729 | ## ERROR ? 1730 | if not response or \ 1731 | ('message' in response and 1732 | response['message'] == 'Forbidden'): 1733 | _invoke_error(channel_list=response['payload'][ 1734 | 'channels'], error=response['message']) 1735 | self.timeout(1, _connect) 1736 | return 1737 | if 'message' in response: 1738 | _invoke_error(error=response['message']) 1739 | else: 1740 | _invoke_disconnect() 1741 | self.timetoken = 0 1742 | self.timeout(1, _connect) 1743 | 1744 | def sub_callback(response): 1745 | ## ERROR ? 1746 | if not response or \ 1747 | ('message' in response and 1748 | response['message'] == 'Forbidden'): 1749 | _invoke_error(channel_list=response['payload'][ 1750 | 'channels'], error=response['message']) 1751 | _connect() 1752 | return 1753 | 1754 | _invoke_connect() 1755 | 1756 | with self._tt_lock: 1757 | self.timetoken = \ 1758 | self.last_timetoken if self.timetoken == 0 and \ 1759 | self.last_timetoken != 0 else response[1] 1760 | 1761 | if len(response) > 3: 1762 | channel_list = response[2].split(',') 1763 | channel_list_2 = response[3].split(',') 1764 | response_list = response[0] 1765 | for ch in enumerate(channel_list): 1766 | if ch[1] in self.subscription_groups or ch[1] in self.subscriptions: 1767 | try: 1768 | chobj = self.subscription_groups[ch[1]] 1769 | except KeyError as k: 1770 | chobj = self.subscriptions[ch[1]] 1771 | _invoke(chobj['callback'], 1772 | self.decrypt(response_list[ch[0]]), 1773 | chobj['name'].split('-pnpres')[0], channel_list_2[ch[0]].split('-pnpres')[0]) 1774 | elif len(response) > 2: 1775 | channel_list = response[2].split(',') 1776 | response_list = response[0] 1777 | for ch in enumerate(channel_list): 1778 | if ch[1] in self.subscriptions: 1779 | chobj = self.subscriptions[ch[1]] 1780 | _invoke(chobj['callback'], 1781 | self.decrypt(response_list[ch[0]]), 1782 | chobj['name'].split('-pnpres')[0]) 1783 | else: 1784 | response_list = response[0] 1785 | chobj = _get_channel() 1786 | for r in response_list: 1787 | if chobj: 1788 | _invoke(chobj['callback'], self.decrypt(r), 1789 | chobj['name'].split('-pnpres')[0]) 1790 | 1791 | _connect() 1792 | 1793 | channel_list = self.get_channel_list(self.subscriptions) 1794 | channel_group_list = self.get_channel_group_list(self.subscription_groups) 1795 | 1796 | if len(channel_list) <= 0 and len(channel_group_list) <= 0: 1797 | return 1798 | 1799 | if len(channel_list) <= 0: 1800 | channel_list = ',' 1801 | 1802 | ## CONNECT TO PUBNUB SUBSCRIBE SERVERS 1803 | #try: 1804 | self.SUB_RECEIVER = self._request({"urlcomponents": [ 1805 | 'subscribe', 1806 | self.subscribe_key, 1807 | channel_list, 1808 | '0', 1809 | str(self.timetoken) 1810 | ], "urlparams": {"uuid": self.uuid, "auth": self.auth_key, 1811 | 'pnsdk' : self.pnsdk, 'channel-group' : channel_group_list}}, 1812 | sub_callback, 1813 | error_callback, 1814 | single=True, timeout=320) 1815 | ''' 1816 | except Exception as e: 1817 | print(e) 1818 | self.timeout(1, _connect) 1819 | return 1820 | ''' 1821 | 1822 | self._connect = _connect 1823 | 1824 | ## BEGIN SUBSCRIPTION (LISTEN FOR MESSAGES) 1825 | _connect() 1826 | 1827 | def _reset_offline(self): 1828 | if self.SUB_RECEIVER is not None: 1829 | self.SUB_RECEIVER() 1830 | self.SUB_RECEIVER = None 1831 | 1832 | def CONNECT(self): 1833 | self._reset_offline() 1834 | self._connect() 1835 | 1836 | def unsubscribe(self, channel): 1837 | """Unsubscribe from channel . 1838 | Only works in async mode 1839 | 1840 | Args: 1841 | channel: Channel name ( string ) 1842 | """ 1843 | if channel in self.subscriptions is False: 1844 | return False 1845 | 1846 | ## DISCONNECT 1847 | with self._channel_list_lock: 1848 | if channel in self.subscriptions: 1849 | self.subscriptions[channel]['connected'] = 0 1850 | self.subscriptions[channel]['subscribed'] = False 1851 | self.subscriptions[channel]['timetoken'] = 0 1852 | self.subscriptions[channel]['first'] = False 1853 | self.leave_channel(channel=channel) 1854 | self.CONNECT() 1855 | 1856 | def unsubscribe_group(self, channel_group): 1857 | """Unsubscribe from channel group. 1858 | Only works in async mode 1859 | 1860 | Args: 1861 | channel_group: Channel group name ( string ) 1862 | """ 1863 | if channel_group in self.subscription_groups is False: 1864 | return False 1865 | 1866 | ## DISCONNECT 1867 | with self._channel_group_list_lock: 1868 | if channel_group in self.subscription_groups: 1869 | self.subscription_groups[channel_group]['connected'] = 0 1870 | self.subscription_groups[channel_group]['subscribed'] = False 1871 | self.subscription_groups[channel_group]['timetoken'] = 0 1872 | self.subscription_groups[channel_group]['first'] = False 1873 | self.leave_group(channel_group=channel_group) 1874 | self.CONNECT() 1875 | 1876 | 1877 | class PubnubCore(PubnubCoreAsync): 1878 | def __init__( 1879 | self, 1880 | publish_key, 1881 | subscribe_key, 1882 | secret_key=None, 1883 | cipher_key=None, 1884 | auth_key=None, 1885 | ssl_on=False, 1886 | origin='pubsub.pubnub.com', 1887 | uuid=None, 1888 | _tt_lock=None, 1889 | _channel_list_lock=None, 1890 | _channel_group_list_lock=None 1891 | 1892 | ): 1893 | super(PubnubCore, self).__init__( 1894 | publish_key=publish_key, 1895 | subscribe_key=subscribe_key, 1896 | secret_key=secret_key, 1897 | cipher_key=cipher_key, 1898 | auth_key=auth_key, 1899 | ssl_on=ssl_on, 1900 | origin=origin, 1901 | uuid=uuid, 1902 | _tt_lock=_tt_lock, 1903 | _channel_list_lock=_channel_list_lock, 1904 | _channel_group_list_lock=_channel_group_list_lock 1905 | ) 1906 | 1907 | self.subscriptions = {} 1908 | self.timetoken = 0 1909 | self.accept_encoding = 'gzip' 1910 | 1911 | def subscribe_sync(self, channel, callback, timetoken=0): 1912 | """ 1913 | #** 1914 | #* Subscribe 1915 | #* 1916 | #* This is BLOCKING. 1917 | #* Listen for a message on a channel. 1918 | #* 1919 | #* @param array args with channel and callback. 1920 | #* @return false on fail, array on success. 1921 | #** 1922 | 1923 | ## Subscribe Example 1924 | def receive(message) : 1925 | print(message) 1926 | return True 1927 | 1928 | pubnub.subscribe({ 1929 | 'channel' : 'hello_world', 1930 | 'callback' : receive 1931 | }) 1932 | 1933 | """ 1934 | 1935 | subscribe_key = self.subscribe_key 1936 | 1937 | ## Begin Subscribe 1938 | while True: 1939 | try: 1940 | ## Wait for Message 1941 | response = self._request({"urlcomponents": [ 1942 | 'subscribe', 1943 | subscribe_key, 1944 | channel, 1945 | '0', 1946 | str(timetoken) 1947 | ], "urlparams": {"uuid": self.uuid, 'pnsdk' : self.pnsdk}}) 1948 | 1949 | messages = response[0] 1950 | timetoken = response[1] 1951 | 1952 | ## If it was a timeout 1953 | if not len(messages): 1954 | continue 1955 | 1956 | ## Run user Callback and Reconnect if user permits. 1957 | for message in messages: 1958 | if not callback(self.decrypt(message)): 1959 | return 1960 | 1961 | except Exception: 1962 | time.sleep(1) 1963 | 1964 | return True 1965 | 1966 | 1967 | class HTTPClient: 1968 | def __init__(self, pubnub, url, urllib_func=None, 1969 | callback=None, error=None, id=None, timeout=5): 1970 | self.url = url 1971 | self.id = id 1972 | self.callback = callback 1973 | self.error = error 1974 | self.stop = False 1975 | self._urllib_func = urllib_func 1976 | self.timeout = timeout 1977 | self.pubnub = pubnub 1978 | 1979 | def cancel(self): 1980 | self.stop = True 1981 | self.callback = None 1982 | self.error = None 1983 | 1984 | def run(self): 1985 | 1986 | def _invoke(func, data): 1987 | if func is not None: 1988 | func(get_data_for_user(data)) 1989 | 1990 | if self._urllib_func is None: 1991 | return 1992 | 1993 | resp = self._urllib_func(self.url, timeout=self.timeout) 1994 | data = resp[0] 1995 | code = resp[1] 1996 | 1997 | if self.stop is True: 1998 | return 1999 | if self.callback is None: 2000 | with self.pubnub.latest_sub_callback_lock: 2001 | if self.pubnub.latest_sub_callback['id'] != self.id: 2002 | return 2003 | else: 2004 | if self.pubnub.latest_sub_callback['callback'] is not None: 2005 | self.pubnub.latest_sub_callback['id'] = 0 2006 | try: 2007 | data = json.loads(data) 2008 | except ValueError as e: 2009 | _invoke(self.pubnub.latest_sub_callback['error'], 2010 | {'error': 'json decoding error'}) 2011 | return 2012 | if code != 200: 2013 | _invoke(self.pubnub.latest_sub_callback['error'], data) 2014 | else: 2015 | _invoke(self.pubnub.latest_sub_callback['callback'], data) 2016 | else: 2017 | try: 2018 | data = json.loads(data) 2019 | except ValueError: 2020 | _invoke(self.error, {'error': 'json decoding error'}) 2021 | return 2022 | 2023 | if code != 200: 2024 | _invoke(self.error, data) 2025 | else: 2026 | _invoke(self.callback, data) 2027 | 2028 | 2029 | def _urllib_request_2(url, timeout=5): 2030 | try: 2031 | resp = urllib2.urlopen(url, timeout=timeout) 2032 | except urllib2.HTTPError as http_error: 2033 | resp = http_error 2034 | except urllib2.URLError as error: 2035 | msg = {"message": str(error.reason)} 2036 | return (json.dumps(msg), 0) 2037 | 2038 | return (resp.read(), resp.code) 2039 | 2040 | class PubnubHTTPAdapter(HTTPAdapter): 2041 | def init_poolmanager(self, *args, **kwargs): 2042 | kwargs.setdefault('socket_options', default_socket_options) 2043 | 2044 | super(PubnubHTTPAdapter, self).init_poolmanager(*args, **kwargs) 2045 | 2046 | s = requests.Session() 2047 | #s.mount('http://', PubnubHTTPAdapter(max_retries=1)) 2048 | #s.mount('https://', PubnubHTTPAdapter(max_retries=1)) 2049 | #s.mount('http://pubsub.pubnub.com', HTTPAdapter(max_retries=1)) 2050 | #s.mount('https://pubsub.pubnub.com', HTTPAdapter(max_retries=1)) 2051 | 2052 | 2053 | def _requests_request(url, timeout=5): 2054 | try: 2055 | resp = s.get(url, timeout=timeout) 2056 | except requests.exceptions.HTTPError as http_error: 2057 | resp = http_error 2058 | except requests.exceptions.ConnectionError as error: 2059 | msg = str(error) 2060 | return (json.dumps(msg), 0) 2061 | except requests.exceptions.Timeout as error: 2062 | msg = str(error) 2063 | return (json.dumps(msg), 0) 2064 | return (resp.text, resp.status_code) 2065 | 2066 | 2067 | def _urllib_request_3(url, timeout=5): 2068 | try: 2069 | resp = urllib.request.urlopen(url, timeout=timeout) 2070 | except (urllib.request.HTTPError, urllib.request.URLError) as http_error: 2071 | resp = http_error 2072 | r = resp.read().decode("utf-8") 2073 | return (r, resp.code) 2074 | 2075 | _urllib_request = None 2076 | 2077 | 2078 | # Pubnub 2079 | 2080 | class Pubnub(PubnubCore): 2081 | def __init__( 2082 | self, 2083 | publish_key, 2084 | subscribe_key, 2085 | secret_key=None, 2086 | cipher_key=None, 2087 | auth_key=None, 2088 | ssl_on=False, 2089 | origin='pubsub.pubnub.com', 2090 | uuid=None, 2091 | pooling=True, 2092 | daemon=False, 2093 | pres_uuid=None, 2094 | azure=False 2095 | ): 2096 | super(Pubnub, self).__init__( 2097 | publish_key=publish_key, 2098 | subscribe_key=subscribe_key, 2099 | secret_key=secret_key, 2100 | cipher_key=cipher_key, 2101 | auth_key=auth_key, 2102 | ssl_on=ssl_on, 2103 | origin=origin, 2104 | uuid=uuid or pres_uuid, 2105 | _tt_lock=threading.RLock(), 2106 | _channel_list_lock=threading.RLock(), 2107 | _channel_group_list_lock=threading.RLock() 2108 | ) 2109 | global _urllib_request 2110 | if self.python_version == 2: 2111 | _urllib_request = _urllib_request_2 2112 | else: 2113 | _urllib_request = _urllib_request_3 2114 | 2115 | if pooling is True: 2116 | _urllib_request = _requests_request 2117 | 2118 | self.latest_sub_callback_lock = threading.RLock() 2119 | self.latest_sub_callback = {'id': None, 'callback': None} 2120 | self.pnsdk = 'PubNub-Python' + '/' + self.version 2121 | self.daemon = daemon 2122 | 2123 | if azure is False: 2124 | s.mount('http://pubsub.pubnub.com', HTTPAdapter(max_retries=1)) 2125 | s.mount('https://pubsub.pubnub.com', HTTPAdapter(max_retries=1)) 2126 | else: 2127 | s.mount('http://', PubnubHTTPAdapter(max_retries=1)) 2128 | s.mount('https://', PubnubHTTPAdapter(max_retries=1)) 2129 | 2130 | def timeout(self, interval, func): 2131 | def cb(): 2132 | time.sleep(interval) 2133 | func() 2134 | thread = threading.Thread(target=cb) 2135 | thread.daemon = self.daemon 2136 | thread.start() 2137 | 2138 | def _request_async(self, request, callback=None, error=None, single=False, timeout=5): 2139 | global _urllib_request 2140 | ## Build URL 2141 | url = self.getUrl(request) 2142 | if single is True: 2143 | id = time.time() 2144 | client = HTTPClient(self, url=url, urllib_func=_urllib_request, 2145 | callback=None, error=None, id=id, timeout=timeout) 2146 | with self.latest_sub_callback_lock: 2147 | self.latest_sub_callback['id'] = id 2148 | self.latest_sub_callback['callback'] = callback 2149 | self.latest_sub_callback['error'] = error 2150 | else: 2151 | client = HTTPClient(self, url=url, urllib_func=_urllib_request, 2152 | callback=callback, error=error, timeout=timeout) 2153 | 2154 | thread = threading.Thread(target=client.run) 2155 | thread.daemon = self.daemon 2156 | thread.start() 2157 | 2158 | def abort(): 2159 | client.cancel() 2160 | return abort 2161 | 2162 | def _request_sync(self, request, timeout=5): 2163 | global _urllib_request 2164 | ## Build URL 2165 | url = self.getUrl(request) 2166 | ## Send Request Expecting JSONP Response 2167 | response = _urllib_request(url, timeout=timeout) 2168 | try: 2169 | resp_json = json.loads(response[0]) 2170 | except ValueError: 2171 | return [0, "JSON Error"] 2172 | 2173 | if response[1] != 200 and 'message' in resp_json and 'payload' in resp_json: 2174 | return {'message': resp_json['message'], 2175 | 'payload': resp_json['payload']} 2176 | 2177 | if response[1] == 0: 2178 | return [0, resp_json] 2179 | 2180 | return resp_json 2181 | 2182 | def _request(self, request, callback=None, error=None, single=False, timeout=5): 2183 | if callback is None: 2184 | return get_data_for_user(self._request_sync(request, timeout=timeout)) 2185 | else: 2186 | self._request_async(request, callback, error, single=single, timeout=timeout) 2187 | 2188 | # Pubnub Twisted 2189 | 2190 | class PubnubTwisted(PubnubCoreAsync): 2191 | 2192 | def start(self): 2193 | reactor.run() 2194 | 2195 | def stop(self): 2196 | reactor.stop() 2197 | 2198 | def timeout(self, delay, callback): 2199 | reactor.callLater(delay, callback) 2200 | 2201 | def __init__( 2202 | self, 2203 | publish_key, 2204 | subscribe_key, 2205 | secret_key=None, 2206 | cipher_key=None, 2207 | auth_key=None, 2208 | ssl_on=False, 2209 | origin='pubsub.pubnub.com' 2210 | ): 2211 | super(PubnubTwisted, self).__init__( 2212 | publish_key=publish_key, 2213 | subscribe_key=subscribe_key, 2214 | secret_key=secret_key, 2215 | cipher_key=cipher_key, 2216 | auth_key=auth_key, 2217 | ssl_on=ssl_on, 2218 | origin=origin, 2219 | ) 2220 | self.headers = {} 2221 | self.headers['User-Agent'] = ['Python-Twisted'] 2222 | self.headers['V'] = [self.version] 2223 | self.pnsdk = 'PubNub-Python-' + 'Twisted' + '/' + self.version 2224 | 2225 | def _request(self, request, callback=None, error=None, single=False, timeout=5): 2226 | global pnconn_pool 2227 | 2228 | def _invoke(func, data): 2229 | if func is not None: 2230 | func(get_data_for_user(data)) 2231 | 2232 | ## Build URL 2233 | 2234 | url = self.getUrl(request) 2235 | 2236 | agent = ContentDecoderAgent(RedirectAgent(Agent( 2237 | reactor, 2238 | contextFactory=WebClientContextFactory(), 2239 | pool=self.ssl and None or pnconn_pool 2240 | )), [('gzip', GzipDecoder)]) 2241 | 2242 | try: 2243 | request = agent.request( 2244 | 'GET', url, Headers(self.headers), None) 2245 | except TypeError as te: 2246 | request = agent.request( 2247 | 'GET', url.encode(), Headers(self.headers), None) 2248 | 2249 | if single is True: 2250 | id = time.time() 2251 | self.id = id 2252 | 2253 | def received(response): 2254 | if not isinstance(response, twisted.web._newclient.Response): 2255 | _invoke(error, {"message": "Not Found"}) 2256 | return 2257 | 2258 | finished = Deferred() 2259 | if response.code in [401, 403]: 2260 | response.deliverBody(PubNubPamResponse(finished)) 2261 | else: 2262 | response.deliverBody(PubNubResponse(finished)) 2263 | 2264 | return finished 2265 | 2266 | def complete(data): 2267 | if single is True: 2268 | if id != self.id: 2269 | return None 2270 | try: 2271 | data = json.loads(data) 2272 | except ValueError as e: 2273 | try: 2274 | data = json.loads(data.decode("utf-8")) 2275 | except ValueError as e: 2276 | _invoke(error, {'error': 'json decode error'}) 2277 | 2278 | if 'error' in data and 'status' in data and 'status' != 200: 2279 | _invoke(error, data) 2280 | else: 2281 | _invoke(callback, data) 2282 | 2283 | def abort(): 2284 | pass 2285 | 2286 | request.addCallback(received) 2287 | request.addCallback(complete) 2288 | 2289 | return abort 2290 | 2291 | 2292 | # PubnubTornado 2293 | class PubnubTornado(PubnubCoreAsync): 2294 | 2295 | def stop(self): 2296 | ioloop.stop() 2297 | 2298 | def start(self): 2299 | ioloop.start() 2300 | 2301 | def timeout(self, delay, callback): 2302 | ioloop.add_timeout(time.time() + float(delay), callback) 2303 | 2304 | def __init__( 2305 | self, 2306 | publish_key, 2307 | subscribe_key, 2308 | secret_key=False, 2309 | cipher_key=False, 2310 | auth_key=False, 2311 | ssl_on=False, 2312 | origin='pubsub.pubnub.com' 2313 | ): 2314 | super(PubnubTornado, self).__init__( 2315 | publish_key=publish_key, 2316 | subscribe_key=subscribe_key, 2317 | secret_key=secret_key, 2318 | cipher_key=cipher_key, 2319 | auth_key=auth_key, 2320 | ssl_on=ssl_on, 2321 | origin=origin, 2322 | ) 2323 | self.headers = {} 2324 | self.headers['User-Agent'] = 'Python-Tornado' 2325 | self.headers['Accept-Encoding'] = self.accept_encoding 2326 | self.headers['V'] = self.version 2327 | self.http = tornado.httpclient.AsyncHTTPClient(max_clients=1000) 2328 | self.id = None 2329 | self.pnsdk = 'PubNub-Python-' + 'Tornado' + '/' + self.version 2330 | 2331 | def _request(self, request, callback=None, error=None, 2332 | single=False, timeout=5, connect_timeout=5): 2333 | 2334 | def _invoke(func, data): 2335 | if func is not None: 2336 | func(get_data_for_user(data)) 2337 | 2338 | url = self.getUrl(request) 2339 | request = tornado.httpclient.HTTPRequest( 2340 | url, 'GET', 2341 | self.headers, 2342 | connect_timeout=connect_timeout, 2343 | request_timeout=timeout) 2344 | if single is True: 2345 | id = time.time() 2346 | self.id = id 2347 | 2348 | def responseCallback(response): 2349 | if single is True: 2350 | if not id == self.id: 2351 | return None 2352 | 2353 | body = response._get_body() 2354 | 2355 | if body is None: 2356 | return 2357 | 2358 | def handle_exc(*args): 2359 | return True 2360 | if response.error is not None: 2361 | with ExceptionStackContext(handle_exc): 2362 | if response.code in [403, 401]: 2363 | response.rethrow() 2364 | else: 2365 | _invoke(error, {"message": response.reason}) 2366 | return 2367 | 2368 | try: 2369 | data = json.loads(body) 2370 | except TypeError as e: 2371 | try: 2372 | data = json.loads(body.decode("utf-8")) 2373 | except ValueError as ve: 2374 | _invoke(error, {'error': 'json decode error'}) 2375 | 2376 | if 'error' in data and 'status' in data and 'status' != 200: 2377 | _invoke(error, data) 2378 | else: 2379 | _invoke(callback, data) 2380 | 2381 | self.http.fetch( 2382 | request=request, 2383 | callback=responseCallback 2384 | ) 2385 | 2386 | def abort(): 2387 | pass 2388 | 2389 | return abort --------------------------------------------------------------------------------