├── instant ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── runws.py │ │ └── installws.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_channel_level.py │ └── 0001_initial.py ├── tests │ ├── __init__.py │ ├── urls.py │ ├── test_utils.py │ ├── test_producers.py │ ├── base.py │ ├── test_token.py │ ├── test_views.py │ ├── test_model.py │ ├── test_conf.py │ └── runtests.py ├── __init__.py ├── admin.py ├── apps.py ├── conf.py ├── urls.py ├── token.py ├── producers.py ├── init.py ├── models.py ├── views.py └── static │ └── instant │ └── index.min.js ├── requirements.txt ├── MANIFEST.in ├── .travis.yml ├── setup.cfg ├── tox.ini ├── pyrightconfig.json ├── .github └── workflows │ └── django.yml ├── .gitignore ├── LICENSE ├── setup.py └── README.md /instant/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /instant/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cent 2 | PyJWT -------------------------------------------------------------------------------- /instant/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /instant/tests/__init__.py: -------------------------------------------------------------------------------- 1 | class InstantTest: 2 | pass 3 | -------------------------------------------------------------------------------- /instant/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.2.0" 2 | default_app_config = "instant.apps.InstantConfig" 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include instant/migrations * 2 | recursive-include instant/management * 3 | recursive-include instant/static * -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 3.6 5 | - 3.7 6 | 7 | env: 8 | global: 9 | - PYTHONPATH="/home/travis/build/synw/django-instant" 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [flake8] 5 | max-line-length = 88 6 | exclude = 7 | .git, 8 | .venv, 9 | build, 10 | __pycache__ 11 | */migrations/* -------------------------------------------------------------------------------- /instant/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | path("instant/", include("instant.urls")), 7 | ] 8 | -------------------------------------------------------------------------------- /instant/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from instant.models import Channel 4 | 5 | 6 | @admin.register(Channel) # type: ignore 7 | class ChannelAdmin(admin.ModelAdmin): 8 | list_display = ["name", "level", "is_active"] 9 | -------------------------------------------------------------------------------- /instant/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.apps import AppConfig 3 | 4 | 5 | class InstantConfig(AppConfig): 6 | name = "instant" 7 | default_auto_field = "django.db.models.BigAutoField" 8 | 9 | def ready(self): 10 | pass 11 | -------------------------------------------------------------------------------- /instant/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | CENTRIFUGO_API_KEY = getattr(settings, "CENTRIFUGO_API_KEY", None) 5 | CENTRIFUGO_HMAC_KEY = getattr(settings, "CENTRIFUGO_HMAC_KEY", None) 6 | CENTRIFUGO_HOST = getattr(settings, "CENTRIFUGO_HOST", "http://localhost") 7 | CENTRIFUGO_PORT = getattr(settings, "CENTRIFUGO_PORT", 8427) 8 | 9 | SITE_SLUG = getattr(settings, "SITE_SLUG", "site") 10 | SITE_NAME = getattr(settings, "SITE_NAME", "Site") 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [base] 2 | deps = 3 | cent 4 | PyJWT 5 | 6 | [testenv:django3] 7 | deps = 8 | django>=3.0 9 | {[base]deps} 10 | 11 | [testenv:coverage] 12 | setenv = 13 | PYTHONPATH = {toxinidir} 14 | commands = 15 | coverage run instant/tests/runtests.py 16 | deps = 17 | coverage 18 | {[testenv:django3]deps} 19 | 20 | [pytest] 21 | DJANGO_SETTINGS_MODULE = instant.tests.runtests 22 | python_files = tests.py test_*.py *_tests.py 23 | 24 | 25 | -------------------------------------------------------------------------------- /instant/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.urls import path 3 | from .views import ( 4 | login_and_get_tokens, 5 | logout, 6 | get_connection_token, 7 | channels_subscription, 8 | ) 9 | 10 | urlpatterns = [ 11 | path("login/", login_and_get_tokens, name="instant-login"), 12 | path("logout/", logout, name="instant-logout"), 13 | path("get_token/", get_connection_token, name="instant-get-token"), 14 | path("subscribe/", channels_subscription, name="instant-subscribe"), 15 | ] 16 | -------------------------------------------------------------------------------- /instant/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from .base import InstantBaseTest 2 | 3 | from instant.init import ensure_channel_is_private 4 | 5 | 6 | class InstantTestUtils(InstantBaseTest): 7 | def test_ensure_channel_is_private(self): 8 | name = ensure_channel_is_private("$chan") 9 | self.assertEqual(name, "$chan") 10 | name = ensure_channel_is_private("chan") 11 | self.assertEqual(name, "$chan") 12 | name = ensure_channel_is_private("ns:$chan") 13 | self.assertEqual(name, "ns:$chan") 14 | name = ensure_channel_is_private("ns:chan") 15 | self.assertEqual(name, "ns:$chan") 16 | -------------------------------------------------------------------------------- /instant/migrations/0002_alter_channel_level.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-01 09:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('instant', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='channel', 15 | name='level', 16 | field=models.CharField(choices=[('public', 'Public'), ('users', 'Users'), ('groups', 'Groups'), ('staff', 'Staff'), ('superuser', 'Superuser')], default='superuser', max_length=20, verbose_name='Authorized for'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "useLibraryCodeForTypes": true, 3 | "reportMissingImports": false, 4 | "reportMissingTypeStubs": false, 5 | "strictSetInference": true, 6 | "strictDictionaryInference": true, 7 | "strictListInference": true, 8 | "reportUntypedFunctionDecorator": true, 9 | "reportUntypedClassDecorator": true, 10 | "reportIncompatibleMethodOverride": "error", 11 | "reportIncompatibleVariableOverride": "error", 12 | "reportUninitializedInstanceVariable": "error", 13 | "reportUnknownParameterType": false, 14 | "reportUnknownVariableType": false, 15 | "reportUnnecessaryCast": "warning", 16 | "reportUnnecessaryComparison": "warning", 17 | } -------------------------------------------------------------------------------- /instant/tests/test_producers.py: -------------------------------------------------------------------------------- 1 | from cent import CentException 2 | 3 | from .base import InstantBaseTest 4 | 5 | from instant.producers import publish 6 | 7 | 8 | class InstantTestProducers(InstantBaseTest): 9 | def test_publish(self): 10 | with self.settings(CENTRIFUGO_API_KEY=None) and self.assertRaises(ValueError): 11 | publish("chan") 12 | with self.assertRaises(CentException): 13 | publish("chan", "message") 14 | with self.assertRaises(CentException): 15 | publish( 16 | "chan", 17 | data={"foo": 1}, 18 | event_class="important", 19 | bucket="testing", 20 | ) 21 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.7, 3.8, 3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install tox 29 | - name: Run Tests 30 | run: | 31 | tox 32 | -------------------------------------------------------------------------------- /instant/management/commands/runws.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import subprocess 4 | 5 | from django.core.management.base import BaseCommand 6 | from instant.conf import CENTRIFUGO_PORT 7 | 8 | 9 | class Command(BaseCommand): 10 | help = "Run the Centrifugo websockets server" 11 | 12 | def handle(self, *args, **options): 13 | basepath = os.getcwd() 14 | c = basepath + "/centrifugo/centrifugo" 15 | conf = basepath + "/centrifugo/config.json" 16 | cmd = [c, "--config", conf, "--port", str(CENTRIFUGO_PORT)] 17 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 18 | for line in io.TextIOWrapper(p.stdout, encoding="utf-8"): # type: ignore 19 | msg = str(line).replace("b'", "") 20 | msg = msg[0:-3] 21 | print(msg) 22 | p.wait() 23 | -------------------------------------------------------------------------------- /instant/token.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import time 3 | 4 | from .conf import CENTRIFUGO_HMAC_KEY 5 | 6 | def connection_token(user): 7 | if CENTRIFUGO_HMAC_KEY is None: 8 | raise ValueError("Provide a CENTRIFUGO_HMAC_KEY in settings") 9 | claims = {"sub": user.get_username(), "exp": int(time.time()) + 24 * 60 * 60} 10 | token = jwt.encode(claims, CENTRIFUGO_HMAC_KEY, algorithm="HS256") 11 | return token 12 | 13 | 14 | def channel_token(channel, user): 15 | if CENTRIFUGO_HMAC_KEY is None: 16 | raise ValueError("Provide a CENTRIFUGO_HMAC_KEY in settings") 17 | claims = { 18 | "sub": user.get_username(), 19 | "channel": channel, 20 | "exp": int(time.time()) + 24 * 60 * 60, 21 | } 22 | token = jwt.encode(claims, CENTRIFUGO_HMAC_KEY, algorithm="HS256") 23 | return token 24 | -------------------------------------------------------------------------------- /instant/tests/base.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from django.test import TestCase 4 | from django.test.client import RequestFactory 5 | from django.contrib.auth.models import User 6 | from django.conf import settings 7 | 8 | from instant.models import Channel 9 | 10 | 11 | class InstantBaseTest(TestCase): 12 | user = None 13 | 14 | def setUp(self): 15 | self.factory = RequestFactory() # type: ignore 16 | self.user = User.objects.create_user( # type: ignore 17 | "myuser", "myemail@test.com", "password" 18 | ) 19 | self.superuser = User.objects.create_superuser( # type: ignore 20 | "superuser", "myemail@test.com", "password" 21 | ) 22 | 23 | @property 24 | def base_dir(self) -> Path: 25 | d = settings.BASE_DIR 26 | if isinstance(d, str): 27 | d = Path(d) 28 | return d 29 | 30 | def reset(self): 31 | for chan in Channel.objects.all(): 32 | chan.delete() 33 | -------------------------------------------------------------------------------- /instant/tests/test_token.py: -------------------------------------------------------------------------------- 1 | from .base import InstantBaseTest 2 | 3 | from instant.token import connection_token, channel_token 4 | 5 | 6 | class InstantTestToken(InstantBaseTest): 7 | def test_connection_token(self): 8 | #with self.assertRaises(ValueError): 9 | # with self.settings(CENTRIFUGO_HMAC_KEY=None): 10 | # connection_token(self.user) 11 | t = connection_token(self.user) 12 | # print(t) 13 | self.assertTrue( 14 | str(t).startswith( 15 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" 16 | ) 17 | ) 18 | 19 | def test_channel_token(self): 20 | #with self.settings(CENTRIFUGO_HMAC_KEY=None) and self.assertRaises(ValueError): 21 | # channel_token("chan", self.user) 22 | t = channel_token("chan", self.user) 23 | # print(t) 24 | self.assertTrue( 25 | str(t).startswith( 26 | ( 27 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." 28 | ) 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /instant/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from .base import InstantBaseTest 2 | 3 | from django.urls import reverse 4 | from django.http import JsonResponse, HttpResponseForbidden 5 | 6 | 7 | class InstantTestViews(InstantBaseTest): 8 | def test_logout_view(self): 9 | self.client.login(username="myuser", password="password") 10 | response = self.client.get(reverse("instant-logout")) 11 | self.assertIsInstance(response, JsonResponse) 12 | self.assertEqual(response.status_code, 200) 13 | self.client.logout() 14 | response = self.client.get(reverse("instant-logout")) 15 | self.assertIsInstance(response, HttpResponseForbidden) 16 | self.assertEqual(response.status_code, 403) 17 | 18 | def test_get_token(self): 19 | response = self.client.get(reverse("instant-get-token")) 20 | self.assertIsInstance(response, HttpResponseForbidden) 21 | # self.client.login(username="myuser", password="password") 22 | # response = self.client.get(reverse("instant-get-token")) 23 | # print("RESP", response) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules/ 2 | settings.py 3 | *.sqlite3 4 | .project 5 | .pydevproject 6 | .settings 7 | coverage_html_report/ 8 | test_db 9 | bin/ 10 | pyvenv.cfg 11 | .coveragerc 12 | .vscode/ 13 | .pytest_cache 14 | 15 | __pycache__/ 16 | *.py[cod] 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | env/ 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *,cover 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 synw 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | this_directory = path.abspath(path.dirname(__file__)) 5 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: 6 | long_description = f.read() 7 | 8 | version = __import__("instant").__version__ 9 | 10 | setup( 11 | name="django-instant", 12 | packages=find_packages(), 13 | include_package_data=True, 14 | version=version, 15 | description="Websockets for Django with Centrifugo ", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | author="synw", 19 | author_email="synwe@yahoo.com", 20 | url="https://github.com/synw/django-instant", 21 | download_url="https://github.com/synw/django-instant/releases/tag/" + version, 22 | keywords=["django", "websockets", "centrifugo"], 23 | classifiers=[ 24 | "Development Status :: 4 - Beta", 25 | "Framework :: Django :: 4.0", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python :: 3.8", 29 | ], 30 | install_requires=["cent", "PyJWT"], 31 | zip_safe=False, 32 | ) 33 | -------------------------------------------------------------------------------- /instant/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | initial = True 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name='Channel', 11 | fields=[ 12 | ('id', models.BigAutoField(auto_created=True, 13 | primary_key=True, serialize=False, verbose_name='ID')), 14 | ('name', models.CharField(help_text='Use $ to prefix non public channels: ex: $private_chan', 15 | max_length=120, unique=True, verbose_name='Name')), 16 | ('level', models.CharField(choices=[('public', 'Public'), ('users', 'Users'), ('groups', 'Groups'), ( 17 | 'staff', 'Staff'), ('superuser', 'Superuser')], max_length=20, verbose_name='Authorized for')), 18 | ('is_active', models.BooleanField( 19 | default=True, verbose_name='Active')), 20 | ('groups', models.ManyToManyField( 21 | blank=True, to='auth.Group', verbose_name='Groups')), 22 | ], 23 | options={ 24 | 'verbose_name': 'Channel', 25 | 'verbose_name_plural': 'Channels', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /instant/tests/test_model.py: -------------------------------------------------------------------------------- 1 | from .base import InstantBaseTest 2 | 3 | from instant.models import Channel 4 | 5 | 6 | class InstantTestCreate(InstantBaseTest): 7 | def test_channels_creation(self): 8 | self.reset() 9 | chan = Channel.objects.create(name="chan") 10 | self.assertTrue(chan.is_active) 11 | self.assertEqual(chan.name, "$chan") 12 | self.assertEqual(chan.level, "superuser") 13 | self.assertEqual(str(chan), "$chan") 14 | 15 | def test_public_channel_creation(self): 16 | chan = Channel.objects.create(name="chan", level="public") 17 | self.assertEqual(chan.name, "chan") 18 | self.assertEqual(chan.level, "public") 19 | 20 | def test_public_channel_creation_enum(self): 21 | chan = Channel.objects.create(name="chan", level=Channel.Level.Public) 22 | self.assertEqual(chan.name, "chan") 23 | self.assertEqual(chan.level, "public") 24 | 25 | 26 | def test_channel_manager(self): 27 | Channel.objects.create(name="$chan") 28 | user_chans = Channel.objects.for_user(self.superuser) #  type: ignore 29 | self.assertEqual(user_chans[0].name, "$chan") 30 | Channel.objects.create(name="chan", level="public") 31 | user_chans = Channel.objects.for_user(self.user) #  type: ignore 32 | self.assertEqual(user_chans[0].name, "chan") 33 | -------------------------------------------------------------------------------- /instant/producers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict, Union 3 | 4 | from cent import Client 5 | 6 | from .conf import SITE_NAME, CENTRIFUGO_HOST, CENTRIFUGO_PORT, CENTRIFUGO_API_KEY 7 | 8 | 9 | def publish( 10 | channel: str, 11 | *args, 12 | event_class: Union[str, None] = None, 13 | data: Union[Dict[str, Any], None] = None, 14 | bucket: str = "", 15 | site: str = SITE_NAME 16 | ) -> None: 17 | """ 18 | Publish a message to a websockets channel 19 | """ 20 | message = None 21 | if len(args) == 1: 22 | message = args[0] 23 | if message is None and data is None: 24 | raise ValueError("Provide either a message or data argument") 25 | cent_url = CENTRIFUGO_HOST 26 | cent_url += ":" + str(CENTRIFUGO_PORT) + "/api" 27 | if CENTRIFUGO_API_KEY is None: 28 | raise ValueError("Provide a CENTRIFUGO_API_KEY in settings") 29 | client = Client( 30 | cent_url, 31 | api_key = CENTRIFUGO_API_KEY, 32 | timeout = 1, 33 | ) 34 | payload: Dict[str, Any] = {"channel": channel, "site": site} 35 | if message is not None: 36 | payload["message"] = message 37 | if data is not None: 38 | payload["data"] = data 39 | if event_class is not None: 40 | payload["event_class"] = event_class 41 | if len(bucket) > 0: 42 | payload["bucket"] = bucket 43 | client.publish(channel, json.dumps(payload)) 44 | -------------------------------------------------------------------------------- /instant/init.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Union 2 | from pathlib import Path 3 | 4 | from django.conf import settings 5 | 6 | 7 | def generate_settings_from_conf( 8 | conf: Dict[str, Any], site_name: Union[str, None] = None 9 | ) -> List[str]: 10 | """ 11 | Generate Django settings from Centrifugo json conf 12 | """ 13 | buffer = [] 14 | buffer.append('CENTRIFUGO_HOST = "http://localhost"') 15 | buffer.append("CENTRIFUGO_PORT = 8427") 16 | buffer.append(f'CENTRIFUGO_HMAC_KEY = "{conf["token_hmac_secret_key"]}"') 17 | buffer.append(f'CENTRIFUGO_API_KEY = "{conf["api_key"]}"') 18 | default_base_dir = settings.BASE_DIR 19 | if isinstance(default_base_dir, Path) is False: 20 | default_base_dir = Path(default_base_dir) 21 | project_name = default_base_dir.name 22 | if site_name is not None: 23 | project_name = site_name 24 | buffer.append(f'SITE_NAME = "{project_name}"') 25 | return buffer 26 | 27 | 28 | def ensure_channel_is_private(chan: str) -> str: 29 | """ 30 | Make sure that a private channel name starts with a $ sign 31 | """ 32 | name = chan 33 | if ":" in name: 34 | names = name.split(":") 35 | prefix = names[0] 36 | suffix = names[1] 37 | if suffix.startswith("$") is False: 38 | return prefix + ":$" + suffix 39 | else: 40 | if chan.startswith("$") is False: 41 | return "$" + chan 42 | return chan 43 | -------------------------------------------------------------------------------- /instant/tests/test_conf.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | from .base import InstantBaseTest 3 | 4 | from instant.init import generate_settings_from_conf 5 | from instant.conf import ( 6 | CENTRIFUGO_API_KEY, 7 | CENTRIFUGO_HMAC_KEY, 8 | CENTRIFUGO_HOST, 9 | CENTRIFUGO_PORT, 10 | SITE_SLUG, 11 | SITE_NAME, 12 | ) 13 | 14 | 15 | class InstantTestConf(InstantBaseTest): 16 | def test_default_conf(self): 17 | self.assertEqual(CENTRIFUGO_API_KEY, "key") 18 | self.assertEqual(CENTRIFUGO_HMAC_KEY, "key") 19 | self.assertEqual(CENTRIFUGO_HOST, "http://localhost") 20 | self.assertEqual(CENTRIFUGO_PORT, 8427) 21 | self.assertEqual(SITE_SLUG, "site") 22 | self.assertEqual(SITE_NAME, "Site") 23 | 24 | def test_generate_settings_from_conf(self): 25 | conf: Dict[str, Any] = {"token_hmac_secret_key": "key", "api_key": "key"} 26 | s = generate_settings_from_conf(conf, "site") 27 | self.assertListEqual( 28 | [ 29 | 'CENTRIFUGO_HOST = "http://localhost"', 30 | "CENTRIFUGO_PORT = 8427", 31 | 'CENTRIFUGO_HMAC_KEY = "key"', 32 | 'CENTRIFUGO_API_KEY = "key"', 33 | 'SITE_NAME = "site"', 34 | ], 35 | s, 36 | ) 37 | s = generate_settings_from_conf(conf) 38 | self.assertListEqual( 39 | [ 40 | 'CENTRIFUGO_HOST = "http://localhost"', 41 | "CENTRIFUGO_PORT = 8427", 42 | 'CENTRIFUGO_HMAC_KEY = "key"', 43 | 'CENTRIFUGO_API_KEY = "key"', 44 | 'SITE_NAME = "tests"', 45 | ], 46 | s, 47 | ) 48 | -------------------------------------------------------------------------------- /instant/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import Group 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .init import ensure_channel_is_private 6 | 7 | class ChannelManager(models.Manager): 8 | def for_user(self, user): 9 | filter_from = [Channel.Level.Public, Channel.Level.Users] 10 | if user.is_superuser: 11 | filter_from.append(Channel.Level.Superuser) 12 | if user.is_staff: 13 | filter_from.append(Channel.Level.Staff) 14 | chans = Channel.objects.filter( 15 | is_active=True, groups__in=user.groups.all() 16 | ) | Channel.objects.filter(level__in=filter_from, is_active=True) 17 | return chans 18 | 19 | 20 | class Channel(models.Model): 21 | class Level(models.TextChoices): 22 | Public = "public" 23 | Users = "users" 24 | Groups = "groups" 25 | Staff = "staff" 26 | Superuser = "superuser" 27 | 28 | name = models.CharField( 29 | max_length=120, 30 | verbose_name=_("Name"), 31 | unique=True, 32 | help_text=_("Use $ to prefix non public channels: " "ex: $private_chan"), 33 | ) 34 | level = models.CharField( 35 | max_length=20, 36 | choices=Level.choices, 37 | verbose_name=_("Authorized for"), 38 | default="superuser", 39 | ) 40 | is_active = models.BooleanField(default=True, verbose_name=_("Active")) 41 | groups = models.ManyToManyField(Group, blank=True, verbose_name=_("Groups")) 42 | objects = ChannelManager() 43 | 44 | class Meta: 45 | verbose_name = _("Channel") 46 | verbose_name_plural = _("Channels") 47 | 48 | def __str__(self) -> str: 49 | return str(self.name) 50 | 51 | def save(self, *args, **kwargs): 52 | # check the channel name 53 | if self.level != "public": 54 | self.name = ensure_channel_is_private(self.__str__()) 55 | return super(Channel, self).save(*args, **kwargs) 56 | -------------------------------------------------------------------------------- /instant/management/commands/installws.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import platform 4 | import subprocess 5 | 6 | from django.core.management.base import BaseCommand 7 | 8 | from instant.init import generate_settings_from_conf 9 | 10 | 11 | class Command(BaseCommand): 12 | help = ( 13 | "Install the Centrifugo websockets server for Linux and Darwin" 14 | "and generate the Django settings" 15 | ) 16 | 17 | def handle(self, *args, **options): 18 | centrifugo_version = "4.0.4" 19 | run_on = str(platform.system()).lower() 20 | suffix = "_linux_amd64" 21 | if run_on == "darwin": 22 | suffix = "_darwin_amd64" 23 | suffix_file = suffix + ".tar.gz" 24 | fetch_url = ( 25 | "https://github.com/centrifugal/centrifugo/releases/download/v" 26 | + centrifugo_version 27 | + "/centrifugo_" 28 | + centrifugo_version 29 | + suffix_file 30 | ) 31 | basepath = os.getcwd() 32 | subprocess.call(["mkdir", "centrifugo"]) 33 | os.chdir(basepath + "/centrifugo") 34 | subprocess.call(["wget", fetch_url]) 35 | name = "centrifugo_" + centrifugo_version + suffix 36 | subprocess.call(["tar", "-xzf", name + ".tar.gz"]) 37 | subprocess.call(["rm", "-f", name + ".tar.gz"]) 38 | subprocess.call(["chmod", "a+x", "centrifugo"]) 39 | subprocess.call(["./centrifugo", "genconfig"]) 40 | # set allowed origins in Centrifugo config 41 | filepath = basepath + "/centrifugo/config.json" 42 | with open(filepath, "r+") as f: 43 | content = f.read() 44 | conf = json.loads(content) 45 | conf["allowed_origins"] = [ 46 | "http://localhost:8000", # django dev server 47 | "http://localhost:3000", # frontend dev server 48 | "http://localhost:5000", # frontend build preview 49 | ] 50 | output = json.dumps(conf, indent=4) 51 | f.seek(0) 52 | f.write(output) 53 | f.truncate() 54 | f.close() 55 | # generate settings 56 | buffer = generate_settings_from_conf(conf) 57 | # print conf 58 | print("\nAppend this to your Django settings:\n") 59 | for line in buffer: 60 | print(line) 61 | print("\n") 62 | print( 63 | "The Centrifugo websockets server is installed. Run it with python " 64 | "manage.py runws" 65 | ) 66 | -------------------------------------------------------------------------------- /instant/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib import auth 4 | from django.http import JsonResponse, HttpResponseForbidden 5 | from django.views.decorators.csrf import csrf_exempt 6 | from django.middleware.csrf import get_token 7 | 8 | from .token import connection_token, channel_token 9 | from .models import Channel 10 | 11 | def user_channels(user): 12 | user_chans = Channel.objects.for_user(user) 13 | authorized_chans = [] 14 | for channel in user_chans: 15 | # print("Checking auth for chan", channel, channel in user_chans_names) 16 | chan = { 17 | "name": channel.name, 18 | "level": channel.level, 19 | "token": channel_token(channel.name, user), 20 | } 21 | authorized_chans.append(chan) 22 | return authorized_chans 23 | 24 | 25 | @csrf_exempt # type: ignore 26 | def channels_subscription(request): 27 | if not request.user.is_authenticated: 28 | return HttpResponseForbidden() 29 | json_data = json.loads(request.body) 30 | # print("PRIVATE SUB for", request.user.username, json_data) 31 | user_chans = Channel.objects.for_user(request.user).values("name") # type: ignore 32 | user_chans_names = [] 33 | for chan in user_chans: 34 | user_chans_names.append(chan["name"]) 35 | authorized_chans = user_channels(request.user) 36 | # print({"channels": authorized_chans}) 37 | return JsonResponse({"channels": authorized_chans}) 38 | 39 | 40 | def _get_response(request): 41 | channels = user_channels(request.user) 42 | return JsonResponse( 43 | { 44 | "csrf_token": get_token(request), 45 | "ws_token": connection_token(request.user), 46 | "channels": channels, 47 | } 48 | ) 49 | 50 | 51 | @csrf_exempt # type: ignore 52 | def login_and_get_tokens(request): 53 | # print("Login view", request.method) 54 | if request.user.is_authenticated: 55 | return _get_response(request) 56 | if request.method == "POST": 57 | # print("POST", request.body) 58 | json_data = json.loads(request.body) 59 | username = json_data["username"] 60 | password = json_data["password"] 61 | # print("Authenticate", username, password) 62 | user = auth.authenticate(username=username, password=password) 63 | if user is not None: 64 | auth.login(request, user) 65 | return _get_response(request) 66 | return HttpResponseForbidden() 67 | 68 | 69 | def logout(request): 70 | if request.user.is_authenticated: 71 | auth.logout(request) 72 | return JsonResponse({"response": "ok"}) 73 | return HttpResponseForbidden() 74 | 75 | 76 | @csrf_exempt # type: ignore 77 | def get_connection_token(request): 78 | if not request.user.is_authenticated: 79 | return HttpResponseForbidden() 80 | return _get_response(request) 81 | -------------------------------------------------------------------------------- /instant/tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | import django 7 | from django.conf import settings 8 | from django.core.management import execute_from_command_line 9 | 10 | 11 | BASE_DIR = Path(os.path.dirname(__file__)) 12 | sys.path.append(BASE_DIR.parent.resolve().as_posix()) 13 | 14 | # Unfortunately, apps can not be installed via ``modify_settings`` 15 | # decorator, because it would miss the database setup. 16 | CUSTOM_INSTALLED_APPS = ("django.contrib.admin", "instant") 17 | 18 | ALWAYS_INSTALLED_APPS = ( 19 | "django.contrib.auth", 20 | "django.contrib.contenttypes", 21 | "django.contrib.sessions", 22 | "django.contrib.messages", 23 | "django.contrib.staticfiles", 24 | ) 25 | 26 | ALWAYS_MIDDLEWARE_CLASSES = ( 27 | "django.contrib.sessions.middleware.SessionMiddleware", 28 | "django.middleware.common.CommonMiddleware", 29 | "django.middleware.csrf.CsrfViewMiddleware", 30 | "django.contrib.auth.middleware.AuthenticationMiddleware", 31 | "django.contrib.messages.middleware.MessageMiddleware", 32 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 33 | ) 34 | 35 | TEMPLATES_DIR = BASE_DIR / "templates" 36 | 37 | TEMPLATES = [ 38 | { 39 | "BACKEND": "django.template.backends.django.DjangoTemplates", 40 | "DIRS": [TEMPLATES_DIR], 41 | "APP_DIRS": True, 42 | "OPTIONS": { 43 | "context_processors": [ 44 | "django.template.context_processors.debug", 45 | "django.template.context_processors.request", 46 | "django.contrib.auth.context_processors.auth", 47 | "django.contrib.messages.context_processors.messages", 48 | ], 49 | }, 50 | }, 51 | ] 52 | 53 | 54 | settings.configure( 55 | BASE_DIR=BASE_DIR, 56 | SECRET_KEY="django_tests_secret_key", 57 | DEBUG=False, 58 | TEMPLATES=TEMPLATES, 59 | TEMPLATE_DEBUG=False, 60 | ALLOWED_HOSTS=[], 61 | INSTALLED_APPS=ALWAYS_INSTALLED_APPS + CUSTOM_INSTALLED_APPS, 62 | MIDDLEWARE=ALWAYS_MIDDLEWARE_CLASSES, 63 | ROOT_URLCONF="tests.urls", 64 | DATABASES={ 65 | "default": { 66 | "ENGINE": "django.db.backends.sqlite3", 67 | "NAME": "test_db", 68 | } 69 | }, 70 | LANGUAGE_CODE="en-us", 71 | TIME_ZONE="UTC", 72 | USE_I18N=True, 73 | USE_L10N=True, 74 | USE_TZ=True, 75 | STATIC_URL="/static/", 76 | # Use a fast hasher to speed up tests. 77 | PASSWORD_HASHERS=("django.contrib.auth.hashers.MD5PasswordHasher",), 78 | FIXTURE_DIRS=BASE_DIR / "*/fixtures/", 79 | MQUEUE_AUTOREGISTER=(("django.contrib.auth.models.User", ["c", "d", "u"]),), 80 | MQUEUE_HOOKS={ 81 | "redis": { 82 | "path": "mqueue.hooks.redis", 83 | "host": "localhost", 84 | "port": 6379, 85 | "db": 0, 86 | }, 87 | }, 88 | DEFAULT_AUTO_FIELD="django.db.models.BigAutoField", 89 | CENTRIFUGO_API_KEY="key", 90 | CENTRIFUGO_HMAC_KEY="key", 91 | ) 92 | 93 | django.setup() 94 | args = [sys.argv[0], "test"] 95 | 96 | # Current module (``tests``) and its submodules. 97 | test_cases = "." 98 | 99 | # Allow accessing test options from the command line. 100 | offset = 1 101 | try: 102 | sys.argv[1] 103 | except IndexError: 104 | pass 105 | else: 106 | option = sys.argv[1].startswith("-") 107 | if not option: 108 | test_cases = sys.argv[1] 109 | offset = 2 110 | 111 | args.append(test_cases) 112 | # ``verbosity`` can be overwritten from command line. 113 | args.append("--verbosity=2") 114 | args.extend(sys.argv[offset:]) 115 | 116 | execute_from_command_line(args) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Instant 2 | 3 | [![pub package](https://img.shields.io/pypi/v/django-instant)](https://pypi.org/project/django-instant/) [![Django CI](https://github.com/synw/django-instant/actions/workflows/django.yml/badge.svg)](https://github.com/synw/django-instant/actions/workflows/django.yml) 4 | 5 | Websockets for Django with [Centrifugo](https://github.com/centrifugal/centrifugo). 6 | 7 | * Push events into public or private channels. 8 | * Handle the events in javascript client-side. 9 | 10 | :sunny: Compatible: plug it on an existing Django instance _without any modification in your main stack_ 11 | 12 | ### Example 13 | 14 | Push events in channels from anywhere in the code: 15 | 16 | ```python 17 | from instant.producers import publish 18 | 19 | # Publish to a public channel 20 | publish("public", "Message for everyone") 21 | 22 | # Publish to a private channel with an event class set 23 | publish("$users", "Message in logged in users channel", event_class="important") 24 | 25 | # Publish to a group channel 26 | publish("$group1", "Message for users in group1") 27 | 28 | # Publish to the staff channel with an extra json data payload 29 | data = {"field1":"value1","field2":[1,2]} 30 | publish("$staff", "Message for staff", data=data) 31 | ``` 32 | 33 | ## Quick start 34 | 35 | ### Install the Django package 36 | 37 | ``` 38 | pip install django-instant 39 | ``` 40 | 41 | Add `"instant"` to `INSTALLED_APPS` and update `urls.py`: 42 | 43 | ```python 44 | urlpatterns = [ 45 | # ... 46 | path("instant/", include("instant.urls")), 47 | ] 48 | ``` 49 | 50 | ### Install the websockets server 51 | 52 | #### Using the installer 53 | 54 | Use the Centrifugo installer management command (for Linux and MacOs): 55 | 56 | ``` 57 | python manage.py installws 58 | ``` 59 | 60 | This will download a Centrifugo binary release and install it under a *centrifugo* directory. It will 61 | generate the Django settings to use. 62 | 63 | #### Install manualy 64 | 65 | Install the Centrifugo websockets server: see the [detailled doc](https://centrifugal.github.io/centrifugo/server/install/) 66 | 67 |
68 |
69 | Download a release https://github.com/centrifugal/centrifugo/releases/latest 70 | and generate a configuration file: 71 | 72 | ``` 73 | ./centrifugo genconfig 74 | ``` 75 | 76 | The generated `config.json` file looks like this: 77 | 78 | ```javascript 79 | { 80 | "v3_use_offset": true, 81 | "token_hmac_secret_key": "46b38493-147e-4e3f-86e0-dc5ec54f5133", 82 | "admin_password": "ad0dff75-3131-4a02-8d64-9279b4f1c57b", 83 | "admin_secret": "583bc4b7-0fa5-4c4a-8566-16d3ce4ad401", 84 | "api_key": "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6", 85 | "allowed_origins": [] 86 | } 87 | ``` 88 |
89 | 90 | ### Configure the Django settings 91 | 92 | Use the parameters from the installer's output or from Centrifugo's `config.json` file 93 | to update your Django's `settings.py`: 94 | 95 | ```python 96 | CENTRIFUGO_HOST = "http://localhost" 97 | CENTRIFUGO_PORT = 8001 98 | CENTRIFUGO_HMAC_KEY = "46b38493-147e-4e3f-86e0-dc5ec54f5133" 99 | CENTRIFUGO_API_KEY = "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6" 100 | SITE_NAME = "My site" # used in the messages to identify where they come from 101 | ``` 102 | 103 | ### Create channels 104 | 105 | Go into the admin to create channels or create them programatically: 106 | 107 | ```python 108 | from instant.models import Channel 109 | 110 | Channel.objects.create(name="superuser", level=Channel.Level.Superuser) 111 | ``` 112 | 113 | Api: channel create parameters: 114 | 115 | - `name`: the channel name. Required and unique 116 | - `level`: access authorization level for a channel: *Public, Users, Groups, Staff, Superuser*. Default: *Superuser* 117 | - `is_active`: a boolean to disable a channel 118 | - `groups`: a list of authorized Django groups for a channel 119 | 120 | ## Avalailable endpoints 121 | 122 | `/instant/login/`: takes a username and password as parameter and will login the 123 | user in Django and return a Centrifugo connection token 124 | 125 | `/instant/get_token/`: get a Centrifugo connection token for a logged in user 126 | 127 | The two methods above return some connection information: a token for 128 | the websockets connection, a Django csrf token and a list of authorized 129 | channels for the user: 130 | 131 | ```javascript 132 | { 133 | "csrf_token": "fvO61oyhcfzrW3SjPCYxYfzDAQFO6Yz7yaAQkxDbhC0NhlwoP1cecqLEYv8SCDLK", 134 | "ws_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJnZ2ciLCJleHAiOjE2M..", 135 | "channels": [ 136 | { 137 | "name": "public", 138 | "level": "public" 139 | }, 140 | { 141 | "name": "$users", 142 | "level": "users" 143 | }, 144 | { 145 | "name": "$group1", 146 | "level": "groups" 147 | } 148 | ] 149 | } 150 | ``` 151 | 152 | `/instant/subscribe/`: get tokens for Centrifugo channels subscriptions 153 | ([doc](https://centrifugal.github.io/centrifugo/server/private_channels/)) 154 | 155 | ## Publish method 156 | 157 | The required parameters are `channel` and either `message` or `data` 158 | 159 | ```python 160 | publish("$users", "A message", data={ 161 | "foo": "bar"}, event_class="important", bucket="notifications") 162 | ``` 163 | 164 | The other parameters are optional 165 | 166 | ## Javascript client 167 | 168 | Several options are available for the client side 169 | 170 | ### Use the official Centrifugo js client 171 | 172 | Manage your websockets connection manually with the official Centrifugo js library: 173 | [Centrifuge-js](https://github.com/centrifugal/centrifuge-js) 174 | 175 | ### Use the embeded client in a script tag 176 | 177 | In a Django template: 178 | 179 | ```html 180 | {% load static %} 181 | 182 | 185 | ``` 186 | 187 | [Api doc](https://github.com/synw/djangoinstant#usage) 188 | 189 | ### Use the npm client 190 | 191 | A dedicated [client](https://github.com/synw/djangoinstant) is available from npm 192 | to handle the messages and connections client side in javascript or typescript 193 | 194 | ## Example 195 | 196 | An [example](https://github.com/synw/django-instant-example) with a backend and a frontend is available 197 | 198 | ## Tests 199 | 200 | To run the tests: 201 | 202 | ```bash 203 | tox 204 | ``` 205 | -------------------------------------------------------------------------------- /instant/static/instant/index.min.js: -------------------------------------------------------------------------------- 1 | var $instant=function(e){"use strict";var t,s="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n={},i={},r={},o={exports:{}},c="object"==typeof Reflect?Reflect:null,a=c&&"function"==typeof c.apply?c.apply:function(e,t,s){return Function.prototype.apply.call(e,t,s)};t=c&&"function"==typeof c.ownKeys?c.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var h=Number.isNaN||function(e){return e!=e};function u(){u.init.call(this)}o.exports=u,o.exports.once=function(e,t){return new Promise((function(s,n){function i(s){e.removeListener(t,r),n(s)}function r(){"function"==typeof e.removeListener&&e.removeListener("error",i),s([].slice.call(arguments))}S(e,t,r,{once:!0}),"error"!==t&&function(e,t,s){"function"==typeof e.on&&S(e,"error",t,s)}(e,i,{once:!0})}))},u.EventEmitter=u,u.prototype._events=void 0,u.prototype._eventsCount=0,u.prototype._maxListeners=void 0;var l=10;function d(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function _(e){return void 0===e._maxListeners?u.defaultMaxListeners:e._maxListeners}function p(e,t,s,n){var i,r,o,c;if(d(s),void 0===(r=e._events)?(r=e._events=Object.create(null),e._eventsCount=0):(void 0!==r.newListener&&(e.emit("newListener",t,s.listener?s.listener:s),r=e._events),o=r[t]),void 0===o)o=r[t]=s,++e._eventsCount;else if("function"==typeof o?o=r[t]=n?[s,o]:[o,s]:n?o.unshift(s):o.push(s),(i=_(e))>0&&o.length>i&&!o.warned){o.warned=!0;var a=new Error("Possible EventEmitter memory leak detected. "+o.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");a.name="MaxListenersExceededWarning",a.emitter=e,a.type=t,a.count=o.length,c=a,console&&console.warn&&console.warn(c)}return e}function b(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function f(e,t,s){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:s},i=b.bind(n);return i.listener=s,n.wrapFn=i,i}function m(e,t,s){var n=e._events;if(void 0===n)return[];var i=n[t];return void 0===i?[]:"function"==typeof i?s?[i.listener||i]:[i]:s?function(e){for(var t=new Array(e.length),s=0;s0&&(r=t[0]),r instanceof Error)throw r;var o=new Error("Unhandled error."+(r?" ("+r.message+")":""));throw o.context=r,o}var c=i[e];if(void 0===c)return!1;if("function"==typeof c)a(c,this,t);else{var h=c.length,u=v(c,h);for(s=0;s=0;r--)if(s[r]===t||s[r].listener===t){o=s[r].listener,i=r;break}if(i<0)return this;0===i?s.shift():function(e,t){for(;t+1=0;n--)this.removeListener(e,t[n]);return this},u.prototype.listeners=function(e){return m(this,e,!0)},u.prototype.rawListeners=function(e){return m(this,e,!1)},u.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):g.call(e,t)},u.prototype.listenerCount=g,u.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]};var y={};Object.defineProperty(y,"__esModule",{value:!0}),y.unsubscribedCodes=y.subscribingCodes=y.disconnectedCodes=y.connectingCodes=y.errorCodes=void 0,y.errorCodes={timeout:1,transportClosed:2,clientDisconnected:3,clientClosed:4,clientConnectToken:5,clientRefreshToken:6,subscriptionUnsubscribed:7,subscriptionSubscribeToken:8,subscriptionRefreshToken:9,transportWriteError:10,connectionClosed:11},y.connectingCodes={connectCalled:0,transportClosed:1,noPing:2,subscribeTimeout:3,unsubscribeError:4},y.disconnectedCodes={disconnectCalled:0,unauthorized:1,badProtocol:2,messageSizeLimit:3},y.subscribingCodes={subscribeCalled:0,transportClosed:1},y.unsubscribedCodes={unsubscribeCalled:0,unauthorized:1,clientClosed:2};var T={};!function(e){var t,s;Object.defineProperty(e,"__esModule",{value:!0}),e.SubscriptionState=e.State=void 0,(t=e.State||(e.State={})).Disconnected="disconnected",t.Connecting="connecting",t.Connected="connected",(s=e.SubscriptionState||(e.SubscriptionState={})).Unsubscribed="unsubscribed",s.Subscribing="subscribing",s.Subscribed="subscribed"}(T);var w={};function C(e){return null!=e&&"function"==typeof e}Object.defineProperty(w,"__esModule",{value:!0}),w.ttlMilliseconds=w.errorExists=w.backoff=w.log=w.isFunction=w.startsWith=void 0,w.startsWith=function(e,t){return 0===e.lastIndexOf(t,0)},w.isFunction=C,w.log=function(e,t){if(globalThis.console){const s=globalThis.console[e];C(s)&&s.apply(globalThis.console,t)}},w.backoff=function(e,t,s){e>31&&(e=31);const n=function(e,t){return Math.floor(Math.random()*(t-e+1)+e)}(0,Math.min(s,t*Math.pow(2,e)));return Math.min(s,t+n)},w.errorExists=function(e){return"error"in e&&null!==e.error},w.ttlMilliseconds=function(e){return Math.min(1e3*e,2147483647)};var k=s&&s.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.Subscription=void 0;const P=k(o.exports),E=y,x=T,R=w;class j extends P.default{constructor(e,t,s){super(),this._resubscribeTimeout=null,this._refreshTimeout=null,this.channel=t,this.state=x.SubscriptionState.Unsubscribed,this._centrifuge=e,this._token=null,this._getToken=null,this._data=null,this._recover=!1,this._offset=null,this._epoch=null,this._recoverable=!1,this._positioned=!1,this._joinLeave=!1,this._minResubscribeDelay=500,this._maxResubscribeDelay=2e4,this._resubscribeTimeout=null,this._resubscribeAttempts=0,this._promises={},this._promiseId=0,this._inflight=!1,this._refreshTimeout=null,this._setOptions(s),this._centrifuge._debugEnabled?(this.on("state",(e=>{this._centrifuge._debug("subscription state",t,e.oldState,"->",e.newState)})),this.on("error",(e=>{this._centrifuge._debug("subscription error",t,e)}))):this.on("error",(function(){Function.prototype()}))}ready(e){return this.state===x.SubscriptionState.Unsubscribed?Promise.reject({code:E.errorCodes.subscriptionUnsubscribed,message:this.state}):this.state===x.SubscriptionState.Subscribed?Promise.resolve():new Promise(((t,s)=>{const n={resolve:t,reject:s};e&&(n.timeout=setTimeout((function(){s({code:E.errorCodes.timeout,message:"timeout"})}),e)),this._promises[this._nextPromiseId()]=n}))}subscribe(){this._isSubscribed()||(this._resubscribeAttempts=0,this._setSubscribing(E.subscribingCodes.subscribeCalled,"subscribe called"))}unsubscribe(){this._setUnsubscribed(E.unsubscribedCodes.unsubscribeCalled,"unsubscribe called",!0)}publish(e){const t=this;return this._methodCall().then((function(){return t._centrifuge.publish(t.channel,e)}))}presence(){const e=this;return this._methodCall().then((function(){return e._centrifuge.presence(e.channel)}))}presenceStats(){const e=this;return this._methodCall().then((function(){return e._centrifuge.presenceStats(e.channel)}))}history(e){const t=this;return this._methodCall().then((function(){return t._centrifuge.history(t.channel,e)}))}_methodCall(){return this._isSubscribed()?Promise.resolve():this._isUnsubscribed()?Promise.reject({code:E.errorCodes.subscriptionUnsubscribed,message:this.state}):new Promise(((e,t)=>{const s=setTimeout((function(){t({code:E.errorCodes.timeout,message:"timeout"})}),this._centrifuge._config.timeout);this._promises[this._nextPromiseId()]={timeout:s,resolve:e,reject:t}}))}_nextPromiseId(){return++this._promiseId}_needRecover(){return!0===this._recover}_isUnsubscribed(){return this.state===x.SubscriptionState.Unsubscribed}_isSubscribing(){return this.state===x.SubscriptionState.Subscribing}_isSubscribed(){return this.state===x.SubscriptionState.Subscribed}_setState(e){if(this.state!==e){const t=this.state;return this.state=e,this.emit("state",{newState:e,oldState:t,channel:this.channel}),!0}return!1}_usesToken(){return null!==this._token||null!==this._getToken}_clearSubscribingState(){this._resubscribeAttempts=0,this._clearResubscribeTimeout()}_clearSubscribedState(){this._clearRefreshTimeout()}_setSubscribed(e){if(!this._isSubscribing())return;this._clearSubscribingState(),e.recoverable&&(this._recover=!0,this._offset=e.offset||0,this._epoch=e.epoch||""),this._setState(x.SubscriptionState.Subscribed);const t=this._centrifuge._getSubscribeContext(this.channel,e);this.emit("subscribed",t),this._resolvePromises();const s=e.publications;if(s&&s.length>0)for(const e in s)s.hasOwnProperty(e)&&this._handlePublication(s[e]);!0===e.expires&&(this._refreshTimeout=setTimeout((()=>this._refresh()),(0,R.ttlMilliseconds)(e.ttl)))}_setSubscribing(e,t){this._isSubscribing()||(this._isSubscribed()&&this._clearSubscribedState(),this._setState(x.SubscriptionState.Subscribing)&&this.emit("subscribing",{channel:this.channel,code:e,reason:t}),this._subscribe(!1,!1))}_subscribe(e,t){if(this._centrifuge._debug("subscribing on",this.channel),this._centrifuge.state!==x.State.Connected&&!e)return this._centrifuge._debug("delay subscribe on",this.channel,"till connected"),null;if(this._usesToken()){if(this._token)return this._sendSubscribe(this._token,t);{if(e)return null;const t=this;return this._getSubscriptionToken().then((function(e){t._isSubscribing()&&(e?(t._token=e,t._sendSubscribe(e,!1)):t._failUnauthorized())})).catch((function(e){t._isSubscribing()&&(t.emit("error",{type:"subscribeToken",channel:t.channel,error:{code:E.errorCodes.subscriptionSubscribeToken,message:void 0!==e?e.toString():""}}),t._scheduleResubscribe())})),null}}return this._sendSubscribe("",t)}_sendSubscribe(e,t){const s={channel:this.channel};if(e&&(s.token=e),this._data&&(s.data=this._data),this._positioned&&(s.positioned=!0),this._recoverable&&(s.recoverable=!0),this._joinLeave&&(s.join_leave=!0),this._needRecover()){s.recover=!0;const e=this._getOffset();e&&(s.offset=e);const t=this._getEpoch();t&&(s.epoch=t)}const n={subscribe:s};return this._inflight=!0,this._centrifuge._call(n,t).then((e=>{this._inflight=!1;const t=e.reply.subscribe;this._handleSubscribeResponse(t),e.next&&e.next()}),(e=>{this._inflight=!1,this._handleSubscribeError(e.error),e.next&&e.next()})),n}_handleSubscribeError(e){this._isSubscribing()&&(e.code!==E.errorCodes.timeout?this._subscribeError(e):this._centrifuge._disconnect(E.connectingCodes.subscribeTimeout,"subscribe timeout",!0))}_handleSubscribeResponse(e){this._isSubscribing()&&this._setSubscribed(e)}_setUnsubscribed(e,t,s){this._isUnsubscribed()||(this._isSubscribed()&&(s&&this._centrifuge._unsubscribe(this),this._clearSubscribedState()),this._isSubscribing()&&this._clearSubscribingState(),this._setState(x.SubscriptionState.Unsubscribed)&&this.emit("unsubscribed",{channel:this.channel,code:e,reason:t}),this._rejectPromises({code:E.errorCodes.subscriptionUnsubscribed,message:this.state}))}_handlePublication(e){const t=this._centrifuge._getPublicationContext(this.channel,e);this.emit("publication",t),e.offset&&(this._offset=e.offset)}_handleJoin(e){const t=this._centrifuge._getJoinLeaveContext(e.info);this.emit("join",{channel:this.channel,info:t})}_handleLeave(e){const t=this._centrifuge._getJoinLeaveContext(e.info);this.emit("leave",{channel:this.channel,info:t})}_resolvePromises(){for(const e in this._promises)this._promises[e].timeout&&clearTimeout(this._promises[e].timeout),this._promises[e].resolve(),delete this._promises[e]}_rejectPromises(e){for(const t in this._promises)this._promises[t].timeout&&clearTimeout(this._promises[t].timeout),this._promises[t].reject(e),delete this._promises[t]}_scheduleResubscribe(){const e=this,t=this._getResubscribeDelay();this._resubscribeTimeout=setTimeout((function(){e._isSubscribing()&&e._subscribe(!1,!1)}),t)}_subscribeError(e){if(this._isSubscribing())if(e.code<100||109===e.code||!0===e.temporary){109===e.code&&(this._token=null);const t={channel:this.channel,type:"subscribe",error:e};this._centrifuge.state===x.State.Connected&&this.emit("error",t),this._scheduleResubscribe()}else this._setUnsubscribed(e.code,e.message,!1)}_getResubscribeDelay(){const e=(0,R.backoff)(this._resubscribeAttempts,this._minResubscribeDelay,this._maxResubscribeDelay);return this._resubscribeAttempts++,e}_setOptions(e){e&&(e.since&&(this._offset=e.since.offset,this._epoch=e.since.epoch,this._recover=!0),e.data&&(this._data=e.data),void 0!==e.minResubscribeDelay&&(this._minResubscribeDelay=e.minResubscribeDelay),void 0!==e.maxResubscribeDelay&&(this._maxResubscribeDelay=e.maxResubscribeDelay),e.token&&(this._token=e.token),e.getToken&&(this._getToken=e.getToken),!0===e.positioned&&(this._positioned=!0),!0===e.recoverable&&(this._recoverable=!0),!0===e.joinLeave&&(this._joinLeave=!0))}_getOffset(){const e=this._offset;return null!==e?e:0}_getEpoch(){const e=this._epoch;return null!==e?e:""}_clearRefreshTimeout(){null!==this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null)}_clearResubscribeTimeout(){null!==this._resubscribeTimeout&&(clearTimeout(this._resubscribeTimeout),this._resubscribeTimeout=null)}_getSubscriptionToken(){this._centrifuge._debug("get subscription token for channel",this.channel);const e={channel:this.channel},t=this._getToken;if(null===t)throw new Error("provide a function to get channel subscription token");return t(e)}_refresh(){this._clearRefreshTimeout();const e=this;this._getSubscriptionToken().then((function(t){if(!e._isSubscribed())return;if(!t)return void e._failUnauthorized();e._token=t;const s={sub_refresh:{channel:e.channel,token:t}};e._centrifuge._call(s).then((t=>{const s=t.reply.sub_refresh;e._refreshResponse(s),t.next&&t.next()}),(t=>{e._refreshError(t.error),t.next&&t.next()}))})).catch((function(t){e.emit("error",{type:"refreshToken",channel:e.channel,error:{code:E.errorCodes.subscriptionRefreshToken,message:void 0!==t?t.toString():""}}),e._refreshTimeout=setTimeout((()=>e._refresh()),e._getRefreshRetryDelay())}))}_refreshResponse(e){this._isSubscribed()&&(this._centrifuge._debug("subscription token refreshed, channel",this.channel),this._clearRefreshTimeout(),!0===e.expires&&(this._refreshTimeout=setTimeout((()=>this._refresh()),(0,R.ttlMilliseconds)(e.ttl))))}_refreshError(e){this._isSubscribed()&&(e.code<100||!0===e.temporary?(this.emit("error",{type:"refresh",channel:this.channel,error:e}),this._refreshTimeout=setTimeout((()=>this._refresh()),this._getRefreshRetryDelay())):this._setUnsubscribed(e.code,e.message,!0))}_getRefreshRetryDelay(){return(0,R.backoff)(0,1e4,2e4)}_failUnauthorized(){this._setUnsubscribed(E.unsubscribedCodes.unauthorized,"unauthorized",!0)}}r.Subscription=j;var O={};Object.defineProperty(O,"__esModule",{value:!0}),O.SockjsTransport=void 0;O.SockjsTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._transport=null}name(){return"sockjs"}subName(){return"sockjs-"+this._transport.transport}emulation(){return!1}supported(){return null!==this.options.sockjs}initialize(e,t){this._transport=new this.options.sockjs(this.endpoint,null,this.options.sockjsOptions),this._transport.onopen=()=>{t.onOpen()},this._transport.onerror=e=>{t.onError(e)},this._transport.onclose=e=>{t.onClose(e)},this._transport.onmessage=e=>{t.onMessage(e.data)}}close(){this._transport.close()}send(e){this._transport.send(e)}};var L={};Object.defineProperty(L,"__esModule",{value:!0}),L.WebsocketTransport=void 0;L.WebsocketTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._transport=null}name(){return"websocket"}subName(){return"websocket"}emulation(){return!1}supported(){return void 0!==this.options.websocket&&null!==this.options.websocket}initialize(e,t){let s="";"protobuf"===e&&(s="centrifuge-protobuf"),this._transport=""!==s?new this.options.websocket(this.endpoint,s):new this.options.websocket(this.endpoint),"protobuf"===e&&(this._transport.binaryType="arraybuffer"),this._transport.onopen=()=>{t.onOpen()},this._transport.onerror=e=>{t.onError(e)},this._transport.onclose=e=>{t.onClose(e)},this._transport.onmessage=e=>{t.onMessage(e.data)}}close(){this._transport.close()}send(e){this._transport.send(e)}};var D={};Object.defineProperty(D,"__esModule",{value:!0}),D.HttpStreamTransport=void 0;D.HttpStreamTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._abortController=null,this._utf8decoder=new TextDecoder,this._protocol="json"}name(){return"http_stream"}subName(){return"http_stream"}emulation(){return!0}_handleErrors(e){if(!e.ok)throw new Error(e.status);return e}_fetchEventTarget(e,t,s){const n=new EventTarget;return(0,e.options.fetch)(t,s).then(e._handleErrors).then((t=>{n.dispatchEvent(new Event("open"));let s="",i=0,r=new Uint8Array;const o=t.body.getReader();return new e.options.readableStream({start:t=>function c(){return o.read().then((({done:o,value:a})=>{if(o)return n.dispatchEvent(new Event("close")),void t.close();try{if("json"===e._protocol)for(s+=e._utf8decoder.decode(a);i{n.dispatchEvent(new Event("error",{detail:e})),n.dispatchEvent(new Event("close"))})),n}supported(){return null!==this.options.fetch&&null!==this.options.readableStream&&"undefined"!=typeof TextDecoder&&"undefined"!=typeof AbortController&&"undefined"!=typeof EventTarget&&"undefined"!=typeof Event&&"undefined"!=typeof MessageEvent&&"undefined"!=typeof Error}initialize(e,t,s){let n,i;this._protocol=e,this._abortController=new AbortController,"json"===e?(n={Accept:"application/json","Content-Type":"application/json"},i=s):(n={Accept:"application/octet-stream","Content-Type":"application/octet-stream"},i=s);const r={method:"POST",headers:n,body:i,mode:"cors",credentials:"same-origin",cache:"no-cache",signal:this._abortController.signal},o=this._fetchEventTarget(this,this.endpoint,r);o.addEventListener("open",(()=>{t.onOpen()})),o.addEventListener("error",(e=>{this._abortController.abort(),t.onError(e)})),o.addEventListener("close",(()=>{this._abortController.abort(),t.onClose({code:4,reason:"connection closed"})})),o.addEventListener("message",(e=>{t.onMessage(e.data)}))}close(){this._abortController.abort()}send(e,t,s){let n,i;const r={session:t,node:s,data:e};"json"===this._protocol?(n={"Content-Type":"application/json"},i=JSON.stringify(r)):(n={"Content-Type":"application/octet-stream"},i=this.options.encoder.encodeEmulationRequest(r));const o={method:"POST",headers:n,body:i,mode:"cors",credentials:"same-origin",cache:"no-cache"};(0,this.options.fetch)(this.options.emulationEndpoint,o)}};var M={};Object.defineProperty(M,"__esModule",{value:!0}),M.SseTransport=void 0;M.SseTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._protocol="json",this._transport=null,this._onClose=null}name(){return"sse"}subName(){return"sse"}emulation(){return!0}supported(){return null!==this.options.eventsource&&null!==this.options.fetch}initialize(e,t,s){let n;n=globalThis&&globalThis.document&&globalThis.document.baseURI?new URL(this.endpoint,globalThis.document.baseURI):new URL(this.endpoint),n.searchParams.append("cf_connect",s);const i=new this.options.eventsource(n.toString(),{});this._transport=i;i.onopen=function(){t.onOpen()},i.onerror=function(e){i.close(),t.onError(e),t.onClose({code:4,reason:"connection closed"})},i.onmessage=function(e){t.onMessage(e.data)},this._onClose=function(){t.onClose({code:4,reason:"connection closed"})}}close(){this._transport.close(),null!==this._onClose&&this._onClose()}send(e,t,s){const n={session:t,node:s,data:e},i={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),mode:"cors",credentials:"same-origin",cache:"no-cache"};(0,this.options.fetch)(this.options.emulationEndpoint,i)}};var U={},I=s&&s.__awaiter||function(e,t,s,n){return new(s||(s=Promise))((function(i,r){function o(e){try{a(n.next(e))}catch(e){r(e)}}function c(e){try{a(n.throw(e))}catch(e){r(e)}}function a(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(o,c)}a((n=n.apply(e,t||[])).next())}))};Object.defineProperty(U,"__esModule",{value:!0}),U.WebtransportTransport=void 0;U.WebtransportTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._transport=null,this._stream=null,this._writer=null,this._utf8decoder=new TextDecoder,this._protocol="json"}name(){return"webtransport"}subName(){return"webtransport"}emulation(){return!1}supported(){return void 0!==this.options.webtransport&&null!==this.options.webtransport}initialize(e,t){return I(this,void 0,void 0,(function*(){let s;s=globalThis&&globalThis.document&&globalThis.document.baseURI?new URL(this.endpoint,globalThis.document.baseURI):new URL(this.endpoint),"protobuf"===e&&s.searchParams.append("cf_protocol","protobuf"),this._protocol=e;const n=new EventTarget;this._transport=new this.options.webtransport(s.toString()),this._transport.closed.then((()=>{t.onClose({code:4,reason:"connection closed"})})).catch((()=>{t.onClose({code:4,reason:"connection closed"})}));try{yield this._transport.ready}catch(e){return void this.close()}let i;try{i=yield this._transport.createBidirectionalStream()}catch(e){return void this.close()}this._stream=i,this._writer=this._stream.writable.getWriter(),n.addEventListener("close",(()=>{t.onClose({code:4,reason:"connection closed"})})),n.addEventListener("message",(e=>{t.onMessage(e.data)})),this._startReading(n),t.onOpen()}))}_startReading(e){return I(this,void 0,void 0,(function*(){const t=this._stream.readable.getReader();let s="",n=0,i=new Uint8Array;try{for(;;){const{done:r,value:o}=yield t.read();if(o.length>0)if("json"===this._protocol)for(s+=this._utf8decoder.decode(o);nJSON.stringify(e))).join("\n")}};W.JsonDecoder=class{decodeReplies(e){return e.trim().split("\n").map((e=>JSON.parse(e)))}};var A=s&&s.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(i,"__esModule",{value:!0}),i.Centrifuge=void 0;const J=r,z=y,N=O,F=L,q=D,B=M,H=U,$=W,K=w,X=T,G=A(o.exports),Q={protocol:"json",token:null,getToken:null,data:null,debug:!1,name:"js",version:"",fetch:null,readableStream:null,websocket:null,eventsource:null,sockjs:null,sockjsOptions:{},emulationEndpoint:"/emulation",minReconnectDelay:500,maxReconnectDelay:2e4,timeout:5e3,maxServerPingDelay:1e4};class V extends G.default{constructor(e,t){super(),this._reconnectTimeout=null,this._refreshTimeout=null,this._serverPingTimeout=null,this.state=X.State.Disconnected,this._endpoint=e,this._emulation=!1,this._transports=[],this._currentTransportIndex=0,this._triedAllTransports=!1,this._transportWasOpen=!1,this._transport=null,this._transportClosed=!0,this._encoder=null,this._decoder=null,this._reconnectTimeout=null,this._reconnectAttempts=0,this._client=null,this._session="",this._node="",this._subs={},this._serverSubs={},this._commandId=0,this._commands=[],this._batching=!1,this._refreshRequired=!1,this._refreshTimeout=null,this._callbacks={},this._token=void 0,this._dispatchPromise=Promise.resolve(),this._serverPing=0,this._serverPingTimeout=null,this._sendPong=!1,this._promises={},this._promiseId=0,this._debugEnabled=!1,this._config=Object.assign(Object.assign({},Q),t),this._configure(),this._debugEnabled?(this.on("state",(e=>{this._debug("client state",e.oldState,"->",e.newState)})),this.on("error",(e=>{this._debug("client error",e)}))):this.on("error",(function(){Function.prototype()}))}newSubscription(e,t){if(null!==this.getSubscription(e))throw new Error("Subscription to the channel "+e+" already exists");const s=new J.Subscription(this,e,t);return this._subs[e]=s,s}getSubscription(e){return this._getSub(e)}removeSubscription(e){e&&(e.state!==X.SubscriptionState.Unsubscribed&&e.unsubscribe(),this._removeSubscription(e))}subscriptions(){return this._subs}ready(e){return this.state===X.State.Disconnected?Promise.reject({code:z.errorCodes.clientDisconnected,message:"client disconnected"}):this.state===X.State.Connected?Promise.resolve():new Promise(((t,s)=>{const n={resolve:t,reject:s};e&&(n.timeout=setTimeout((function(){s({code:z.errorCodes.timeout,message:"timeout"})}),e)),this._promises[this._nextPromiseId()]=n}))}connect(){this._isConnected()?this._debug("connect called when already connected"):this._isConnecting()?this._debug("connect called when already connecting"):(this._reconnectAttempts=0,this._startConnecting())}disconnect(){this._disconnect(z.disconnectedCodes.disconnectCalled,"disconnect called",!1)}send(e){const t={send:{data:e}},s=this;return this._methodCall().then((function(){return s._transportSendCommands([t])?Promise.resolve():Promise.reject(s._createErrorObject(z.errorCodes.transportWriteError,"transport write error"))}))}rpc(e,t){const s={rpc:{method:e,data:t}},n=this;return this._methodCall().then((function(){return n._callPromise(s,(function(e){return{data:e.rpc.data}}))}))}publish(e,t){const s={publish:{channel:e,data:t}},n=this;return this._methodCall().then((function(){return n._callPromise(s,(function(){return{}}))}))}history(e,t){const s={history:this._getHistoryRequest(e,t)},n=this;return this._methodCall().then((function(){return n._callPromise(s,(function(t){const s=t.history,i=[];if(s.publications)for(let t=0;t=this._transports.length&&(this._triedAllTransports=!0,this._currentTransportIndex=0);let r=0;for(;;){if(r>=this._transports.length)throw new Error("no supported transport found");const o=this._transports[this._currentTransportIndex],c=o.transport,a=o.endpoint;if("websocket"===c){if(this._debug("trying websocket transport"),this._transport=new F.WebsocketTransport(a,{websocket:e}),!this._transport.supported()){this._debug("websocket transport not available"),this._currentTransportIndex++,r++;continue}}else if("webtransport"===c){if(this._debug("trying webtransport transport"),this._transport=new H.WebtransportTransport(a,{webtransport:globalThis.WebTransport,decoder:this._decoder,encoder:this._encoder}),!this._transport.supported()){this._debug("webtransport transport not available"),this._currentTransportIndex++,r++;continue}}else if("http_stream"===c){if(this._debug("trying http_stream transport"),this._transport=new q.HttpStreamTransport(a,{fetch:n,readableStream:i,emulationEndpoint:this._config.emulationEndpoint,decoder:this._decoder,encoder:this._encoder}),!this._transport.supported()){this._debug("http_stream transport not available"),this._currentTransportIndex++,r++;continue}}else if("sse"===c){if(this._debug("trying sse transport"),this._transport=new B.SseTransport(a,{eventsource:s,fetch:n,emulationEndpoint:this._config.emulationEndpoint}),!this._transport.supported()){this._debug("sse transport not available"),this._currentTransportIndex++,r++;continue}}else{if("sockjs"!==c)throw new Error("unknown transport "+c);if(this._debug("trying sockjs"),this._transport=new N.SockjsTransport(a,{sockjs:t,sockjsOptions:this._config.sockjsOptions}),!this._transport.supported()){this._debug("sockjs transport not available"),this._currentTransportIndex++,r++;continue}}break}}else{if((0,K.startsWith)(this._endpoint,"http"))throw new Error("Provide explicit transport endpoints configuration in case of using HTTP (i.e. using array of TransportEndpoint instead of a single string), or use ws(s):// scheme in an endpoint if you aimed using WebSocket transport");if(this._debug("client will use websocket"),this._transport=new F.WebsocketTransport(this._endpoint,{websocket:e}),!this._transport.supported())throw new Error("WebSocket not available")}const r=this;let o,c=!1,a=!0;"sse"===this._transport.name()&&(a=!1);const h=[];if(this._transport.emulation()){const e=r._sendConnect(!0);if(h.push(e),a){const e=r._sendSubscribeCommands(!0,!0);for(const t in e)h.push(e[t])}}const u=this._encoder.encodeCommands(h);this._transport.initialize(this._config.protocol,{onOpen:function(){c=!0,o=r._transport.subName(),r._debug(o,"transport open"),r._transportWasOpen=!0,r._transportClosed=!1,r._transport.emulation()||(r.startBatching(),r._sendConnect(!1),a&&r._sendSubscribeCommands(!0,!1),r.stopBatching())},onError:function(e){r._debug("transport level error",e)},onClose:function(e){r._debug(r._transport.name(),"transport closed"),r._transportClosed=!0;let t="connection closed",s=!0,n=0;if(e&&"code"in e&&e.code&&(n=e.code),e&&e.reason)try{const n=JSON.parse(e.reason);t=n.reason,s=n.reconnect}catch(i){t=e.reason,(n>=3500&&n<4e3||n>=4500&&n<5e3)&&(s=!1)}n<3e3?(1009===n?(n=z.disconnectedCodes.messageSizeLimit,t="message size limit exceeded",s=!1):(n=z.connectingCodes.transportClosed,t="transport closed"),r._emulation&&!r._transportWasOpen&&(r._currentTransportIndex++,r._currentTransportIndex>=r._transports.length&&(r._triedAllTransports=!0,r._currentTransportIndex=0))):r._transportWasOpen=!0;let i=!1;if(!r._emulation||r._transportWasOpen||r._triedAllTransports||(i=!0),r._isConnecting()&&!c&&r.emit("error",{type:"transport",error:{code:z.errorCodes.transportClosed,message:"transport closed"},transport:r._transport.name()}),r._disconnect(n,t,s),r._isConnecting()){let e=r._getReconnectDelay();i&&(e=0),r._debug("reconnect after "+e+" milliseconds"),r._reconnectTimeout=setTimeout((()=>{r._startReconnecting()}),e)}},onMessage:function(e){r._dataReceived(e)}},u)}_sendConnect(e){const t=this._constructConnectCommand(),s=this;return this._call(t,e).then((e=>{const t=e.reply.connect;s._connectResponse(t),e.next&&e.next()}),(e=>{s._connectError(e.error),e.next&&e.next()})),t}_startReconnecting(){if(!this._isConnecting())return;if(!(this._refreshRequired||!this._token&&null!==this._config.getToken))return void this._initializeTransport();const e=this;this._getToken().then((function(t){e._isConnecting()&&(t?(e._token=t,e._debug("connection token refreshed"),e._initializeTransport()):e._failUnauthorized())})).catch((function(t){if(!e._isConnecting())return;e.emit("error",{type:"connectToken",error:{code:z.errorCodes.clientConnectToken,message:void 0!==t?t.toString():""}});const s=e._getReconnectDelay();e._debug("error on connection token refresh, reconnect after "+s+" milliseconds",t),e._reconnectTimeout=setTimeout((()=>{e._startReconnecting()}),s)}))}_connectError(e){this.state===X.State.Connecting&&(109===e.code&&(this._refreshRequired=!0),e.code<100||!0===e.temporary||109===e.code?(this.emit("error",{type:"connect",error:e}),this._transport&&!this._transportClosed&&(this._transportClosed=!0,this._transport.close())):this._disconnect(e.code,e.message,!1))}_constructConnectCommand(){const e={};this._token&&(e.token=this._token),this._config.data&&(e.data=this._config.data),this._config.name&&(e.name=this._config.name),this._config.version&&(e.version=this._config.version);const t={};let s=!1;for(const e in this._serverSubs)if(this._serverSubs.hasOwnProperty(e)&&this._serverSubs[e].recoverable){s=!0;const n={recover:!0};this._serverSubs[e].offset&&(n.offset=this._serverSubs[e].offset),this._serverSubs[e].epoch&&(n.epoch=this._serverSubs[e].epoch),t[e]=n}return s&&(e.subs=t),{connect:e}}_getHistoryRequest(e,t){const s={channel:e};return void 0!==t&&(t.since&&(s.since={offset:t.since.offset},t.since.epoch&&(s.since.epoch=t.since.epoch)),void 0!==t.limit&&(s.limit=t.limit),!0===t.reverse&&(s.reverse=!0)),s}_methodCall(){return this._isConnected()?Promise.resolve():new Promise(((e,t)=>{const s=setTimeout((function(){t({code:z.errorCodes.timeout,message:"timeout"})}),this._config.timeout);this._promises[this._nextPromiseId()]={timeout:s,resolve:e,reject:t}}))}_callPromise(e,t){return new Promise(((s,n)=>{this._call(e,!1).then((e=>{s(t(e.reply)),e.next&&e.next()}),(e=>{n(e.error),e.next&&e.next()}))}))}_dataReceived(e){this._serverPing>0&&this._waitServerPing();const t=this._decoder.decodeReplies(e);this._dispatchPromise=this._dispatchPromise.then((()=>{let e;this._dispatchPromise=new Promise((t=>{e=t})),this._dispatchSynchronized(t,e)}))}_dispatchSynchronized(e,t){let s=Promise.resolve();for(const t in e)e.hasOwnProperty(t)&&(s=s.then((()=>this._dispatchReply(e[t]))));s=s.then((()=>{t()}))}_dispatchReply(e){let t;const s=new Promise((e=>{t=e}));if(null==e)return this._debug("dispatch: got undefined or null reply"),t(),s;const n=e.id;return n&&n>0?this._handleReply(e,t):e.push?this._handlePush(e.push,t):this._handleServerPing(t),s}_call(e,t){return new Promise(((s,n)=>{e.id=this._nextCommandId(),this._registerCall(e.id,s,n),t||this._addCommand(e)}))}_startConnecting(){this._debug("start connecting"),this._setState(X.State.Connecting)&&this.emit("connecting",{code:z.connectingCodes.connectCalled,reason:"connect called"}),this._client=null,this._startReconnecting()}_disconnect(e,t,s){if(this._isDisconnected())return;const n=this.state,i={code:e,reason:t};let r=!1;s?r=this._setState(X.State.Connecting):(r=this._setState(X.State.Disconnected),this._rejectPromises({code:z.errorCodes.clientDisconnected,message:"disconnected"})),this._clearOutgoingRequests(),n===X.State.Connecting&&this._clearReconnectTimeout(),n===X.State.Connected&&this._clearConnectedState(),r&&(this._isConnecting()?this.emit("connecting",i):this.emit("disconnected",i)),this._transport&&!this._transportClosed&&(this._transportClosed=!0,this._transport.close())}_failUnauthorized(){this._disconnect(z.disconnectedCodes.unauthorized,"unauthorized",!1)}_getToken(){if(this._debug("get connection token"),!this._config.getToken)throw new Error("provide a function to get connection token");return this._config.getToken({})}_refresh(){const e=this._client,t=this;this._getToken().then((function(s){if(e!==t._client)return;if(!s)return void t._failUnauthorized();if(t._token=s,t._debug("connection token refreshed"),!t._isConnected())return;const n={refresh:{token:t._token}};t._call(n,!1).then((e=>{const s=e.reply.refresh;t._refreshResponse(s),e.next&&e.next()}),(e=>{t._refreshError(e.error),e.next&&e.next()}))})).catch((function(e){t.emit("error",{type:"refreshToken",error:{code:z.errorCodes.clientRefreshToken,message:void 0!==e?e.toString():""}}),t._refreshTimeout=setTimeout((()=>t._refresh()),t._getRefreshRetryDelay())}))}_refreshError(e){e.code<100||!0===e.temporary?(this.emit("error",{type:"refresh",error:e}),this._refreshTimeout=setTimeout((()=>this._refresh()),this._getRefreshRetryDelay())):this._disconnect(e.code,e.message,!1)}_getRefreshRetryDelay(){return(0,K.backoff)(0,5e3,1e4)}_refreshResponse(e){this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null),e.expires&&(this._client=e.client,this._refreshTimeout=setTimeout((()=>this._refresh()),(0,K.ttlMilliseconds)(e.ttl)))}_removeSubscription(e){null!==e&&delete this._subs[e.channel]}_unsubscribe(e){if(!this._isConnected())return;const t={unsubscribe:{channel:e.channel}},s=this;this._call(t,!1).then((e=>{e.next&&e.next()}),(e=>{e.next&&e.next(),s._disconnect(z.connectingCodes.unsubscribeError,"unsubscribe error",!0)}))}_getSub(e){const t=this._subs[e];return t||null}_isServerSub(e){return void 0!==this._serverSubs[e]}_sendSubscribeCommands(e,t){const s=[];for(const n in this._subs){if(!this._subs.hasOwnProperty(n))continue;const i=this._subs[n];if(!0!==i._inflight&&i.state===X.SubscriptionState.Subscribing){const n=i._subscribe(e,t);n&&s.push(n)}}return s}_connectResponse(e){if(this._transportWasOpen=!0,this._reconnectAttempts=0,this._refreshRequired=!1,this._isConnected())return;this._client=e.client,this._setState(X.State.Connected),this._refreshTimeout&&clearTimeout(this._refreshTimeout),e.expires&&(this._refreshTimeout=setTimeout((()=>this._refresh()),(0,K.ttlMilliseconds)(e.ttl))),this._session=e.session,this._node=e.node,this.startBatching(),this._sendSubscribeCommands(!1,!1),this.stopBatching();const t={client:e.client,transport:this._transport.subName()};e.data&&(t.data=e.data),this.emit("connected",t),this._resolvePromises(),this._processServerSubs(e.subs||{}),e.ping&&e.ping>0?(this._serverPing=1e3*e.ping,this._sendPong=!0===e.pong,this._waitServerPing()):this._serverPing=0}_processServerSubs(e){for(const t in e){if(!e.hasOwnProperty(t))continue;const s=e[t];this._serverSubs[t]={offset:s.offset,epoch:s.epoch,recoverable:s.recoverable||!1};const n=this._getSubscribeContext(t,s);this.emit("subscribed",n)}for(const t in e){if(!e.hasOwnProperty(t))continue;const s=e[t];if(s.recovered){const e=s.publications;if(e&&e.length>0)for(const s in e)e.hasOwnProperty(s)&&this._handlePublication(t,e[s])}}for(const t in this._serverSubs)this._serverSubs.hasOwnProperty(t)&&(e[t]||(this.emit("unsubscribed",{channel:t}),delete this._serverSubs[t]))}_clearRefreshTimeout(){null!==this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null)}_clearReconnectTimeout(){null!==this._reconnectTimeout&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null)}_clearServerPingTimeout(){null!==this._serverPingTimeout&&(clearTimeout(this._serverPingTimeout),this._serverPingTimeout=null)}_waitServerPing(){0!==this._config.maxServerPingDelay&&this._isConnected()&&(this._clearServerPingTimeout(),this._serverPingTimeout=setTimeout((()=>{this._isConnected()&&this._disconnect(z.connectingCodes.noPing,"no ping",!0)}),this._serverPing+this._config.maxServerPingDelay))}_getSubscribeContext(e,t){const s={channel:e,positioned:!1,recoverable:!1,wasRecovering:!1,recovered:!1};t.recovered&&(s.recovered=!0),t.positioned&&(s.positioned=!0),t.recoverable&&(s.recoverable=!0),t.was_recovering&&(s.wasRecovering=!0);let n="";"epoch"in t&&(n=t.epoch);let i=0;return"offset"in t&&(i=t.offset),(s.positioned||s.recoverable)&&(s.streamPosition={offset:i,epoch:n}),t.data&&(s.data=t.data),s}_handleReply(e,t){const s=e.id;if(!(s in this._callbacks))return void t();const n=this._callbacks[s];if(clearTimeout(this._callbacks[s].timeout),delete this._callbacks[s],(0,K.errorExists)(e)){const s=n.errback;if(!s)return void t();s({error:e.error,next:t})}else{const s=n.callback;if(!s)return;s({reply:e,next:t})}}_handleJoin(e,t){const s=this._getSub(e);if(s)s._handleJoin(t);else if(this._isServerSub(e)){const s={channel:e,info:this._getJoinLeaveContext(t.info)};this.emit("join",s)}}_handleLeave(e,t){const s=this._getSub(e);if(s)s._handleLeave(t);else if(this._isServerSub(e)){const s={channel:e,info:this._getJoinLeaveContext(t.info)};this.emit("leave",s)}}_handleUnsubscribe(e,t){const s=this._getSub(e);s?t.code<2500?s._setUnsubscribed(t.code,t.reason,!1):s._setSubscribing(t.code,t.reason):this._isServerSub(e)&&(delete this._serverSubs[e],this.emit("unsubscribed",{channel:e}))}_handleSubscribe(e,t){this._serverSubs[e]={offset:t.offset,epoch:t.epoch,recoverable:t.recoverable||!1},this.emit("subscribed",this._getSubscribeContext(e,t))}_handleDisconnect(e){const t=e.code;let s=!0;(t>=3500&&t<4e3||t>=4500&&t<5e3)&&(s=!1),this._disconnect(t,e.reason,s)}_getPublicationContext(e,t){const s={channel:e,data:t.data};return t.offset&&(s.offset=t.offset),t.info&&(s.info=this._getJoinLeaveContext(t.info)),t.tags&&(s.tags=t.tags),s}_getJoinLeaveContext(e){const t={client:e.client,user:e.user};return e.conn_info&&(t.connInfo=e.conn_info),e.chan_info&&(t.chanInfo=e.chan_info),t}_handlePublication(e,t){const s=this._getSub(e);if(s)s._handlePublication(t);else if(this._isServerSub(e)){const s=this._getPublicationContext(e,t);this.emit("publication",s),void 0!==t.offset&&(this._serverSubs[e].offset=t.offset)}}_handleMessage(e){this.emit("message",{data:e.data})}_handleServerPing(e){if(this._sendPong){const e={};this._transportSendCommands([e])}e()}_handlePush(e,t){const s=e.channel;e.pub?this._handlePublication(s,e.pub):e.message?this._handleMessage(e.message):e.join?this._handleJoin(s,e.join):e.leave?this._handleLeave(s,e.leave):e.unsubscribe?this._handleUnsubscribe(s,e.unsubscribe):e.subscribe?this._handleSubscribe(s,e.subscribe):e.disconnect&&this._handleDisconnect(e.disconnect),t()}_flush(){const e=this._commands.slice(0);this._commands=[],this._transportSendCommands(e)}_createErrorObject(e,t,s){const n={code:e,message:t};return s&&(n.temporary=!0),n}_registerCall(e,t,s){this._callbacks[e]={callback:t,errback:s,timeout:null},this._callbacks[e].timeout=setTimeout((()=>{delete this._callbacks[e],(0,K.isFunction)(s)&&s({error:this._createErrorObject(z.errorCodes.timeout,"timeout")})}),this._config.timeout)}_addCommand(e){this._batching?this._commands.push(e):this._transportSendCommands([e])}_nextPromiseId(){return++this._promiseId}_resolvePromises(){for(const e in this._promises)this._promises[e].timeout&&clearTimeout(this._promises[e].timeout),this._promises[e].resolve(),delete this._promises[e]}_rejectPromises(e){for(const t in this._promises)this._promises[t].timeout&&clearTimeout(this._promises[t].timeout),this._promises[t].reject(e),delete this._promises[t]}}i.Centrifuge=V,V.SubscriptionState=X.SubscriptionState,V.State=X.State,function(e){var t=s&&s.__createBinding||(Object.create?function(e,t,s,n){void 0===n&&(n=s);var i=Object.getOwnPropertyDescriptor(t,s);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,n,i)}:function(e,t,s,n){void 0===n&&(n=s),e[n]=t[s]}),n=s&&s.__exportStar||function(e,s){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(s,n)||t(s,e,n)};Object.defineProperty(e,"__esModule",{value:!0}),e.Subscription=e.Centrifuge=void 0;const o=i;Object.defineProperty(e,"Centrifuge",{enumerable:!0,get:function(){return o.Centrifuge}});const c=r;Object.defineProperty(e,"Subscription",{enumerable:!0,get:function(){return c.Subscription}}),n(T,e)}(n);class Y{channelName;msg;data;eventClass;site;bucket;date;constructor(e){this.channelName=e.channel,this.site=e.site,this.msg=void 0===e.message?"":e.message,this.data=void 0===e.data?{}:e.data,this.eventClass=void 0===e.event_class?"":e.event_class,this.bucket=void 0===e.bucket?"":e.bucket,this.date=new Date}}return e.Message=Y,e.useInstant=()=>{let e,t,s,i="";const r=new Set;let o,c=e=>console.log(JSON.stringify(e,null," "));const a=new Promise((e=>{o=e})),h=async()=>{const e=l({}),s=t+"/instant/get_token/",n=await fetch(s,e);if(!n.ok)throw console.log("Response not ok",n),new Error(n.statusText);const r=await n.json();return d(r),i=r.csrf_token,r.ws_token},u=()=>{r.forEach((t=>{console.log("Subscribing to",t);const s=e.newSubscription(t.name,{token:t.token});s.on("error",(function(e){console.log("subscription error",e)})),s.on("publication",(function(e){const t=_(JSON.parse(e.data));c(t)})),s.subscribe()}))},l=e=>{const t={method:"post",credentials:"include",mode:"cors",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}};return""!==i&&(t.headers={"Content-Type":"application/json","X-CSRFToken":i}),t},d=e=>{console.log("Tokens",JSON.stringify(e,null," ")),i=e.csrf_token,e.channels.forEach((e=>{r.add(e)}))},_=e=>{let t;try{t=new Y(e)}catch(t){throw new Error(`Can not process message ${e} ${t}`)}return t};return{init:async(i,r,c=!1)=>{s=r,t=i;const a=await h();e=new n.Centrifuge(`${s}/connection/websocket`,{token:a,debug:c}),o(!0)},connect:async(t=!0)=>{let s;const n=new Promise((e=>{s=e}));e.on("connected",(function(){s(!0)})),e.connect(),await n,t&&u()},onMessage:e=>{c=e},login:async(e,s)=>{console.log("Login");const n=l({username:e,password:s}),i=t+"/instant/login/",r=await fetch(i,n);if(!r.ok)throw console.log("Response not ok",r),new Error(r.statusText)},subscribe:()=>{u()},getClient:()=>e,onReady:a,channels:r}},Object.defineProperty(e,"__esModule",{value:!0}),e}({}); 2 | --------------------------------------------------------------------------------