2

I want to do the following thing: I want to make a websocket, which prints me all events about esports and I want to use https://sofascore.com I've inspected the network requests as usual and it seems that I need to send a Auth WebSocket Content first, then one for subscribing the right sport and then I will receive my events I need.

I've wrote the following code:

import websockets
import asyncio
from websockets.extensions import permessage_deflate


async def esports():
    async with websockets.connect('wss://ws.sofascore.com:9222/', compression='deflate') as websocket:
        msg = await websocket.recv()
        print(f"From Server: {msg}")
        t = await websocket.send(
            'CONNECT {"no_responders":true,"protocol":1,"verbose":false,"pedantic":false,"user":"none","pass":"none","lang":"nats.ws","version":"1.8.1","headers":true}')
        await websocket.send("PING")
        pong = await websocket.recv()
        print(f"From Server: {pong}")
        await websocket.send(
            'SUB sport.esports 6')
        while (True):
            msg = await websocket.recv()
            print(f"From Server: {msg}")



asyncio.get_event_loop().run_until_complete(esports())

I know that the websocket is compressed as permessage_deflate, when I saw into the request headers of the websocket.

But I still get an error:

Traceback (most recent call last):
  File "C:\Users\Coding\Desktop\websockett.py", line 23, in <module>
    asyncio.get_event_loop().run_until_complete(esports())
  File "C:\Users\Coding\AppData\Local\Programs\Python\Python39-32\lib\asyncio\base_events.py", line 642, in run_until_complete
    return future.result()
  File "C:\Users\Coding\Desktop\websockett.py", line 15, in esports
    await websocket.send(
  File "C:\Users\Coding\AppData\Roaming\Python\Python39\site-packages\websockets\legacy\protocol.py", line 620, in send
    await self.ensure_open()
  File "C:\Users\Coding\AppData\Roaming\Python\Python39\site-packages\websockets\legacy\protocol.py", line 921, in ensure_open
    raise self.connection_closed_exc()
websockets.exceptions.ConnectionClosedError: received 1008 (policy violation) Authentication Timeout; then sent 1008 (policy violation) Authentication Timeout

Process finished with exit code 1

EDIT:

I have now found out that the whole thing works with the Nats network. Is there any way to use Nats with a Libary that also supports the websockets?

Haven't found one on github or pypi unfortunately....

CodingLion
  • 84
  • 9

1 Answers1

1

Ideally you would be able to use the nats-py library:

import asyncio
import nats


async def handler(msg):
    print(f"From server: {msg}")


async def main():
    nc = await nats.connect("wss://ws.sofascore.com:9222")
    await nc.subscribe("sport.esports", cb=handler)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    try:
        loop.run_forever()
    finally:
        loop.close()

However, this library does not currently support connecting with WebSockets, so the above doesn't work (yet - it looks like it's being worked on right now).

For your code, the only reason it fails is that the messages you're sending don't end with \r\n, which the NATS protocol requires. The code works as expected with this change:

import asyncio
import websockets


async def esports():
    async with websockets.connect('wss://ws.sofascore.com:9222') as websocket:
        msg = await websocket.recv()
        print(f"From Server: {msg}")

        await websocket.send(
            'CONNECT {"no_responders":true,"protocol":1,"verbose":false,"pedantic":false,"user":"none","pass":"none","lang":"nats.ws","version":"1.8.1","headers":true}\r\n'
        )

        await websocket.send("SUB sport.esports 1\r\n")

        async for msg in websocket:
            print(f"From Server: {msg}")


asyncio.run(esports())

Of course this will eventually get disconnected because it doesn't respond to PING messages. Here's a little more fleshed out script which implements enough of the NATS protocol to log the sport.esports messages:

import asyncio
import json
import textwrap

from dataclasses import dataclass

import websockets


class SofaError(Exception):
    pass


def message_string(message, data=None, pretty=False):
    s = message
    if data is not None:
        if pretty:
            s += json.dumps(data, indent=2)
        else:
            s += json.dumps(data, separators=(",", ":"))
    return s


def log(pre, message, data=None):
    print(textwrap.indent(message_string(message, data, True), pre))


def recv_log(message, data=None):
    log("< ", message, data)


async def send(websocket, message, data=None):
    log("> ", message, data)
    data = (message_string(message, data, False) + "\r\n").encode()
    await websocket.send(data)


async def connect_and_subscribe(websocket):
    connect_options = {
        "no_responders": True,
        "protocol": 1,
        "verbose": False,
        "pedantic": False,
        "user": "none",
        "pass": "none",
        "lang": "nats.ws",
        "version": "1.8.1",
        "headers": True,
    }
    await send(websocket, "CONNECT ", connect_options)
    await send(websocket, "SUB sport.esports 1")


@dataclass
class NatsMsg:
    subject: str
    sid: str
    reply_to: str
    size: int
    payload: bytes


def parse_msg(info_line, pending_data):
    if not info_line:
        raise SofaError("No payload information received")

    info = [b.decode(errors="replace") for b in info_line.split(b" ")]

    if len(info) == 3:
        subject, sid, size = info
        reply_to = None
    elif len(info) == 4:
        subject, sid, reply_to, size = info
    else:
        raise SofaError("Unrecognized info format")

    try:
        size = int(size)
    except ValueError:
        raise SofaError("Bad payload size")

    if len(pending_data) < size:
        raise SofaError("Incomplete payload")

    payload = pending_data[:size]
    pending_data = pending_data[size:]

    return NatsMsg(subject, sid, reply_to, size, payload), pending_data


async def handler(websocket, ws_message, connected):
    while len(ws_message):
        nats_message, _, ws_message = ws_message.partition(b"\r\n")
        if not nats_message:
            continue

        op, _, rest = nats_message.partition(b" ")

        if op == b"-ERR":
            recv_log(nats_message.decode(errors="replace"))
            err = rest.strip(b"'").decode(errors="replace") if rest else "(No message received)"
            raise SofaError(f"Server error: {err}")

        elif op == b"INFO":
            info_options = json.loads(rest) if rest else None
            recv_log("INFO ", info_options)

            if not connected:
                await connect_and_subscribe(websocket)
                connected = True

        elif op == b"PING":
            recv_log("PING")
            await send(websocket, "PONG")

        elif op == b"MSG":
            try:
                msg, ws_message = parse_msg(rest, ws_message)
            except SofaError as e:
                recv_log(f"MSG (Error: {e}) {rest}")
                continue

            msg_info = (
                f"MSG subject={msg.subject} sid={msg.sid} "
                f"reply-to={msg.reply_to} nbytes={msg.size}:\n"
            )

            try:
                decoded = msg.payload.decode()
                data = json.loads(decoded)
            except UnicodeError:
                recv_log(f"{msg_info}{msg.payload}")
            except json.JSONDecodeError:
                recv_log(f"{msg_info}{decoded}")
            else:
                recv_log(msg_info, data)

        else:
            recv_log(f"(Unhandled op) {nats_message.decode(errors='replace')}")

    return connected


async def main():
    async with websockets.connect("wss://ws.sofascore.com:9222") as websocket:
        connected = False
        async for message in websocket:
            connected = await handler(websocket, message, connected)


if __name__ == "__main__":
    asyncio.run(main())
Josh Brobst
  • 1,772
  • 10
  • 16