├── python ├── archived │ ├── 0.1.9.1 │ │ ├── cloudlink │ │ │ ├── __main__.py │ │ │ ├── __init__.py │ │ │ ├── server │ │ │ │ ├── __init__.py │ │ │ │ └── protocols │ │ │ │ │ └── __init__.py │ │ │ ├── old_client │ │ │ │ └── __init__.py │ │ │ ├── async_client │ │ │ │ └── __init__.py │ │ │ ├── docs │ │ │ │ └── docs.txt │ │ │ ├── cloudlink.py │ │ │ └── supporter.py │ │ ├── requirements.txt │ │ ├── client-test-async.py │ │ ├── client-test-old.py │ │ └── server_example.py │ ├── 0.1.5 │ │ ├── cloudlink │ │ │ └── __init__.py │ │ ├── server_example.py │ │ └── client_example.py │ ├── 0.1.7 │ │ ├── cloudlink │ │ │ └── __init__.py │ │ ├── LICENSE │ │ ├── server_example.py │ │ └── client_example.py │ ├── 0.1.8.3 │ │ ├── cloudlink │ │ │ ├── __init__.py │ │ │ ├── client │ │ │ │ ├── __init__.py │ │ │ │ ├── clientInternalHandlers.py │ │ │ │ ├── clientRootHandlers.py │ │ │ │ └── client.py │ │ │ ├── server │ │ │ │ ├── __init__.py │ │ │ │ ├── websocket_server │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── thread.py │ │ │ │ └── server.py │ │ │ ├── docs │ │ │ │ └── docs.txt │ │ │ └── cloudlink.py │ │ ├── server-example.py │ │ └── client-example.py │ ├── 0.1.9.2 │ │ ├── cloudlink │ │ │ ├── __init__.py │ │ │ ├── server │ │ │ │ ├── __init__.py │ │ │ │ └── protocols │ │ │ │ │ └── __init__.py │ │ │ ├── old_client │ │ │ │ └── __init__.py │ │ │ ├── async_client │ │ │ │ └── __init__.py │ │ │ ├── docs │ │ │ │ └── docs.txt │ │ │ ├── __main__.py │ │ │ └── cloudlink.py │ │ ├── requirements.txt │ │ ├── client-test-async.py │ │ ├── client-test-old.py │ │ └── server_example.py │ ├── 0.1.8.1 │ │ ├── cloudlink │ │ │ ├── __init__.py │ │ │ ├── websocket_server │ │ │ │ ├── __init__.py │ │ │ │ └── thread.py │ │ │ ├── cloudlink.py │ │ │ ├── server.py │ │ │ └── serverRootHandlers.py │ │ └── example.py │ ├── 0.1.8.2 │ │ ├── cloudlink │ │ │ ├── __init__.py │ │ │ ├── websocket_server │ │ │ │ ├── __init__.py │ │ │ │ └── thread.py │ │ │ ├── cloudlink.py │ │ │ └── server.py │ │ └── example.py │ └── 0.1.8.0 │ │ └── cloudlink │ │ ├── websocket_server │ │ ├── __init__.py │ │ └── thread.py │ │ ├── server.py │ │ ├── cloudlink.py │ │ └── serverRootHandlers.py ├── cloudlink │ ├── server │ │ ├── protocols │ │ │ ├── clpv4 │ │ │ │ ├── __init__.py │ │ │ │ └── schema.py │ │ │ ├── scratch │ │ │ │ ├── __init__.py │ │ │ │ └── schema.py │ │ │ └── __init__.py │ │ └── modules │ │ │ ├── __init__.py │ │ │ ├── clients_manager.py │ │ │ └── rooms_manager.py │ ├── __init__.py │ ├── async_iterables.py │ └── client │ │ ├── protocol.py │ │ └── schema.py ├── requirements.txt ├── client_example.py ├── README.md └── server_example.py ├── .gitignore ├── suite └── README.md ├── .github └── dependabot.yml ├── golang ├── README.md ├── server │ ├── client.go │ ├── messages.go │ ├── packet.go │ ├── sessions.go │ ├── scratch.go │ └── manager.go ├── go.mod ├── LICENSE ├── main.go └── go.sum ├── scratch └── README.md ├── serverlist.json ├── LICENSE ├── README.md └── SECURITY.md /python/archived/0.1.9.1/cloudlink/__main__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/archived/0.1.5/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import * 2 | -------------------------------------------------------------------------------- /python/archived/0.1.7/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import * 2 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import * -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import * -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import * -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import * -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import * -------------------------------------------------------------------------------- /python/archived/0.1.9.1/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | websocket-client -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import * -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import * -------------------------------------------------------------------------------- /python/archived/0.1.9.2/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | websocket-client -------------------------------------------------------------------------------- /python/archived/0.1.8.1/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import Cloudlink -------------------------------------------------------------------------------- /python/archived/0.1.8.2/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import Cloudlink -------------------------------------------------------------------------------- /python/cloudlink/server/protocols/clpv4/__init__.py: -------------------------------------------------------------------------------- 1 | from .clpv4 import * 2 | -------------------------------------------------------------------------------- /python/cloudlink/server/protocols/scratch/__init__.py: -------------------------------------------------------------------------------- 1 | from .scratch import * -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/old_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .old_client import * -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/old_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .old_client import * -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/docs/docs.txt: -------------------------------------------------------------------------------- 1 | https://hackmd.io/g6BogABhT6ux1GA2oqaOXA -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/async_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .async_client import * -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/docs/docs.txt: -------------------------------------------------------------------------------- 1 | https://hackmd.io/g6BogABhT6ux1GA2oqaOXA -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/async_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .async_client import * -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/docs/docs.txt: -------------------------------------------------------------------------------- 1 | https://hackmd.io/g6BogABhT6ux1GA2oqaOXA -------------------------------------------------------------------------------- /python/cloudlink/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import server 2 | from .client import client 3 | -------------------------------------------------------------------------------- /python/archived/0.1.8.0/cloudlink/websocket_server/__init__.py: -------------------------------------------------------------------------------- 1 | from .websocket_server import * 2 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | snowflake-id==0.0.2 2 | cerberus==1.3.4 3 | websockets==10.4 4 | ujson==5.7.0 5 | -------------------------------------------------------------------------------- /python/cloudlink/server/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .clients_manager import * 2 | from .rooms_manager import * 3 | -------------------------------------------------------------------------------- /python/cloudlink/server/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | from .clpv4.clpv4 import * 2 | from .scratch.scratch import * 3 | -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/server/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | from .cl_methods import * 2 | from .scratch_methods import * -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/server/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | from .cl_methods import * 2 | from .scratch_methods import * -------------------------------------------------------------------------------- /python/archived/0.1.8.1/cloudlink/websocket_server/__init__.py: -------------------------------------------------------------------------------- 1 | from .thread import * 2 | from .websocket_server import * 3 | -------------------------------------------------------------------------------- /python/archived/0.1.8.2/cloudlink/websocket_server/__init__.py: -------------------------------------------------------------------------------- 1 | from .thread import * 2 | from .websocket_server import * 3 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/server/websocket_server/__init__.py: -------------------------------------------------------------------------------- 1 | from .thread import * 2 | from .websocket_server import * 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | *.bak 4 | .idea 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | *.test 11 | *.out 12 | go.work 13 | *.pem 14 | *.zip -------------------------------------------------------------------------------- /suite/README.md: -------------------------------------------------------------------------------- 1 | # CloudLink Suite 2 | A re-implementation of the original CloudLink Suite (CloudAccount, CloudDisk, CloudCoin) for use with CloudLink 4. 3 | 4 | Work-In-Progress! 5 | -------------------------------------------------------------------------------- /python/archived/0.1.5/server_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # CloudLink 3.0 - Server Mode Example Code 4 | 5 | from cloudlink import CloudLink 6 | import time 7 | 8 | if __name__ == "__main__": 9 | cl = CloudLink() # Instanciate the module 10 | try: 11 | cl.host(port=3000) # Start the module in server mode, host on port 3000 12 | 13 | while cl.mode == 1: # Some other spaghetti code to keep the script running while the connection is live 14 | time.sleep(0.1) 15 | 16 | except KeyboardInterrupt: 17 | cl.stop() # Stops the server and exits -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/golang" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "pip" 14 | directory: "/python" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /python/archived/0.1.7/LICENSE: -------------------------------------------------------------------------------- 1 | 0BSD License 2 | Copyright (C) 2020-2021 MikeDEV Software, Co. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 7 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 8 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 9 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 10 | -------------------------------------------------------------------------------- /golang/README.md: -------------------------------------------------------------------------------- 1 | # CloudLink Golang 2 | This is a *work-in-progress* port of CloudLink to the Go Programming Language! 3 | 4 | ## 💡 Features 💡 5 | 6 | ### 🪶 Even faster and more lightweight 7 | CloudLink Golang can run on (abysmally) minimal resources. At least 10MB of RAM and a potato CPU can run a CloudLink Golang server! 8 | 9 | ### 📦 Minimal dependencies 10 | Use `go mod tidy`. 11 | * 🔵 Golang >=1.20 12 | * 📃 [goccy/go-json](https://github.com/goccy/go-json) 13 | * 🪪 [google/uuid](https://github.com/google/uuid) 14 | * ❄️ [bwmarrin/snowflake](https://github.com/bwmarrin/snowflake) 15 | * 🌐 [gofiber/contrib/websocket](https://github.com/gofiber/contrib/tree/main/websocket) 16 | * 🌐 [gofiber/fiber](https://github.com/gofiber/fiber) 17 | -------------------------------------------------------------------------------- /golang/server/client.go: -------------------------------------------------------------------------------- 1 | package cloudlink 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/gofiber/contrib/websocket" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // The client struct serves as a template for handling websocket sessions. It stores a client's UUID, Snowflake ID, manager and websocket connection pointer(s). 12 | type Client struct { 13 | connection *websocket.Conn 14 | connectionMutex sync.RWMutex 15 | manager *Manager 16 | id snowflake.ID 17 | uuid uuid.UUID 18 | username interface{} 19 | protocol uint8 // 0 - Unset, 1 - CL4, 2 - Scratch 20 | rooms map[interface{}]*Room 21 | handshake bool 22 | 23 | // Lock state for rooms 24 | sync.RWMutex 25 | } 26 | -------------------------------------------------------------------------------- /python/cloudlink/async_iterables.py: -------------------------------------------------------------------------------- 1 | """ 2 | async_iterable - converts a list or set of methods into an asyncio iterable 3 | which can be used in the async for function. 4 | 5 | to use, init the class with the server parent and the list/set of functions. 6 | 7 | import async_iterable 8 | ... 9 | async for event in async_iterable(parent, [foo, bar]): 10 | await event() 11 | """ 12 | 13 | 14 | class async_iterable: 15 | def __init__(self, iterables): 16 | self.iterator = 0 17 | self.iterable = list(iterables) 18 | 19 | def __aiter__(self): 20 | return self 21 | 22 | async def __anext__(self): 23 | if self.iterator >= len(self.iterable): 24 | self.iterator = 0 25 | raise StopAsyncIteration 26 | 27 | self.iterator += 1 28 | 29 | return self.iterable[self.iterator - 1] 30 | -------------------------------------------------------------------------------- /scratch/README.md: -------------------------------------------------------------------------------- 1 | # 🐈 CloudLink Scratch 2 | The original Scratch-based extension! 3 | 4 | # 📃 Supported IDEs 5 | - [PenguinMod](https://studio.penguinmod.com/editor.html?extension=https://extensions.penguinmod.com/extensions/MikeDev101/cloudlink.js) _(Click and run!)_ 6 | - [TurboWarp](https://turbowarp.org/editor?extension=https://extensions.turbowarp.org/cloudlink.js) _(Click and run!)_ 7 | - [SheepTester's E羊icques](https://sheeptester.github.io/scratch-gui) _(You'll need to [inject](https://chrome.google.com/webstore/detail/code-injector/edkcmfocepnifkbnbkmlcmegedeikdeb) the [extension code](https://mikedev101.github.io/cloudlink/scratch/cloudlink_epicques.js) manually.)_ 8 | 9 | _Currently not supported (I'm working on it!):_ 10 | - [Ogadaki's Adacraft](https://adacraft.org/studio/) 11 | - [Ogadaki's Adacraft (Beta)](https://beta.adacraft.org/studio/) 12 | -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/__main__.py: -------------------------------------------------------------------------------- 1 | from .cloudlink import cloudlink 2 | 3 | 4 | class example_events: 5 | def __init__(self): 6 | pass 7 | 8 | async def on_close(self, client): 9 | print("Client", client.id, "disconnected.") 10 | 11 | async def on_connect(self, client): 12 | print("Client", client.id, "connected.") 13 | 14 | 15 | if __name__ == "__main__": 16 | cl = cloudlink() 17 | server = cl.server(logs=True) 18 | events = example_events() 19 | server.set_motd("CL4 Demo Server", True) 20 | server.bind_event(server.events.on_connect, events.on_connect) 21 | server.bind_event(server.events.on_close, events.on_close) 22 | print("Welcome to Cloudlink 4! See https://github.com/mikedev101/cloudlink for more info. Now running server on ws://127.0.0.1:3000/!") 23 | server.run(ip="localhost", port=3000) -------------------------------------------------------------------------------- /serverlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": { 3 | "id": "Localhost", 4 | "url": "ws://127.0.0.1:3000/" 5 | }, 6 | "1": { 7 | "id": "Mike's Server (For Project Compatibility)", 8 | "url": "wss://cl.mikedev101.cc/" 9 | }, 10 | "3": { 11 | "id": "Mike's Server (For Project Compatibility)", 12 | "url": "wss://cl.mikedev101.cc/" 13 | }, 14 | "4": { 15 | "id": "MubiLop's CL4 Server", 16 | "url": "wss://cl1.mubilop.tech/" 17 | }, 18 | "5": { 19 | "id": "Mike's Server (For Project Compatibility)", 20 | "url": "wss://cl.mikedev101.cc/" 21 | }, 22 | "6": { 23 | "id": "Mike's Server (For Project Compatibility)", 24 | "url": "wss://cl.mikedev101.cc/" 25 | }, 26 | "7": { 27 | "id": "Mike's Server", 28 | "url": "wss://cl.mikedev101.cc/" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /golang/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mikedev101/cloudlink/golang 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bwmarrin/snowflake v0.3.0 7 | github.com/goccy/go-json v0.10.2 8 | github.com/gofiber/contrib/websocket v1.2.2 9 | github.com/gofiber/fiber/v2 v2.52.0 10 | github.com/google/uuid v1.5.0 11 | ) 12 | 13 | require ( 14 | github.com/andybalholm/brotli v1.0.5 // indirect 15 | github.com/fasthttp/websocket v1.5.4 // indirect 16 | github.com/klauspost/compress v1.17.0 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mattn/go-runewidth v0.0.15 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect 22 | github.com/valyala/bytebufferpool v1.0.0 // indirect 23 | github.com/valyala/fasthttp v1.51.0 // indirect 24 | github.com/valyala/tcplisten v1.0.0 // indirect 25 | golang.org/x/sys v0.15.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2023 Mike J. Renaker / "MikeDEV". 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /python/archived/0.1.5/client_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # CloudLink 3.0 - Client Mode Example Code 4 | 5 | from cloudlink import CloudLink 6 | import time 7 | 8 | def on_new_packet(message): # message value is automatically converted into a dictionary datatype 9 | print(message) 10 | 11 | def on_connect(): # use to start other scripts, in this example we declare a username: "test" 12 | cl.sendPacket({"cmd": "setid", "val": "test"}) 13 | 14 | def on_error(error): # does this do something? 15 | print(error) 16 | 17 | if __name__ == "__main__": 18 | cl = CloudLink() # Instanciate the module 19 | try: 20 | cl.client("ws://127.0.0.1:3000/", 21 | on_new_packet = on_new_packet, 22 | on_connect = on_connect, 23 | on_error = on_error) # Start the module in client mode, and define our callbacks 24 | 25 | while cl.mode == 2: # Some other spaghetti code to keep the script running while the connection is live 26 | time.sleep(0.1) 27 | 28 | except KeyboardInterrupt: 29 | cl.stop() # Stops the client and exits -------------------------------------------------------------------------------- /python/archived/0.1.8.1/cloudlink/cloudlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | CloudLink Server 5 | 6 | CloudLink is a free and open-source, websocket-powered API optimized for Scratch 3.0. 7 | For documentation, please visit https://hackmd.io/g6BogABhT6ux1GA2oqaOXA 8 | 9 | Server based on https://github.com/Pithikos/python-websocket-server 10 | The WebsocketServer that is bundled with CloudLink is modified to support Cloudflared and fixes an issue with asserting the HTTP websocket upgrade request. 11 | 12 | Please see https://github.com/MikeDev101/cloudlink for more details. 13 | """ 14 | 15 | class Cloudlink: 16 | def __init__(self): 17 | self.version = "0.1.8.1" 18 | 19 | def server(self, logs=False): 20 | # Initialize Cloudlink server 21 | from .server import server 22 | return server(self, logs) 23 | 24 | def client(self, server_ip = "ws://127.0.0.1:3000/", logs=False): 25 | # TODO 26 | pass 27 | 28 | def relay(self, server_ip = "ws://127.0.0.1:3000/", relay_ip = "127.0.0.1", relay_port = 3000, logs=False): 29 | # TODO 30 | pass -------------------------------------------------------------------------------- /python/archived/0.1.8.2/cloudlink/cloudlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | CloudLink Server 5 | 6 | CloudLink is a free and open-source, websocket-powered API optimized for Scratch 3.0. 7 | For documentation, please visit https://hackmd.io/g6BogABhT6ux1GA2oqaOXA 8 | 9 | Server based on https://github.com/Pithikos/python-websocket-server 10 | The WebsocketServer that is bundled with CloudLink is modified to support Cloudflared and fixes an issue with asserting the HTTP websocket upgrade request. 11 | 12 | Please see https://github.com/MikeDev101/cloudlink for more details. 13 | """ 14 | 15 | class Cloudlink: 16 | def __init__(self): 17 | self.version = "0.1.8.2" 18 | 19 | def server(self, logs=False): 20 | # Initialize Cloudlink server 21 | from .server import server 22 | return server(self, logs) 23 | 24 | def client(self, server_ip = "ws://127.0.0.1:3000/", logs=False): 25 | # TODO 26 | pass 27 | 28 | def relay(self, server_ip = "ws://127.0.0.1:3000/", relay_ip = "127.0.0.1", relay_port = 3000, logs=False): 29 | # TODO 30 | pass 31 | -------------------------------------------------------------------------------- /golang/server/messages.go: -------------------------------------------------------------------------------- 1 | package cloudlink 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/goccy/go-json" 8 | "github.com/gofiber/contrib/websocket" 9 | ) 10 | 11 | func JSONDump(message any) []byte { 12 | payload, _ := json.Marshal(message) 13 | return payload 14 | } 15 | 16 | // MulticastMessage broadcasts a payload to multiple clients. 17 | func MulticastMessage(clients map[snowflake.ID]*Client, message any) { 18 | for _, client := range clients { 19 | // Spawn goroutines to multicast the payload 20 | UnicastMessage(client, message) 21 | } 22 | } 23 | 24 | // UnicastMessageAny broadcasts a payload to a singular client. 25 | func UnicastMessage(client *Client, message any) { 26 | // Lock state for the websocket connection to prevent accidental concurrent writes to websocket 27 | client.connectionMutex.Lock() 28 | // Attempt to send message to client 29 | if err := client.connection.WriteMessage(websocket.TextMessage, JSONDump(message)); err != nil { 30 | log.Printf("Client %s (%s) TX error: %s", client.id, client.uuid, err) 31 | } 32 | client.connectionMutex.Unlock() 33 | } 34 | -------------------------------------------------------------------------------- /golang/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mike J. Renaker / "MikeDEV" 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 | -------------------------------------------------------------------------------- /golang/server/packet.go: -------------------------------------------------------------------------------- 1 | package cloudlink 2 | 3 | // This structure represents the JSON formatting used for the current CloudLink formatting scheme. 4 | // Values that are not specific to one type are represented with interface{}. 5 | type PacketUPL struct { 6 | Cmd string `json:"cmd"` 7 | Name interface{} `json:"name,omitempty"` 8 | Val interface{} `json:"val,omitempty"` 9 | ID interface{} `json:"id,omitempty"` 10 | Rooms interface{} `json:"rooms,omitempty"` 11 | Listener interface{} `json:"listener,omitempty"` 12 | Code string `json:"code,omitempty"` 13 | CodeID int `json:"code_id,omitempty"` 14 | Mode string `json:"mode,omitempty"` 15 | Origin *UserObject `json:"origin,omitempty"` 16 | Details string `json:"details,omitempty"` 17 | } 18 | 19 | // This structure represents the JSON formatting the Scratch cloud variable protocol uses. 20 | // Values that are not specific to one type are represented with interface{}. 21 | type Scratch struct { 22 | Method string `json:"method"` 23 | ProjectID interface{} `json:"project_id,omitempty"` 24 | Username string `json:"user,omitempty"` 25 | Value interface{} `json:"value"` 26 | Name interface{} `json:"name,omitempty"` 27 | NewName interface{} `json:"new_name,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /python/archived/0.1.8.0/cloudlink/websocket_server/thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class ThreadWithLoggedException(threading.Thread): 5 | """ 6 | Similar to Thread but will log exceptions to passed logger. 7 | 8 | Args: 9 | logger: Logger instance used to log any exception in child thread 10 | 11 | Exception is also reachable via .exception from the main thread. 12 | """ 13 | 14 | DIVIDER = "*"*80 15 | 16 | def __init__(self, *args, **kwargs): 17 | try: 18 | self.logger = kwargs.pop("logger") 19 | except KeyError: 20 | raise Exception("Missing 'logger' in kwargs") 21 | super().__init__(*args, **kwargs) 22 | self.exception = None 23 | 24 | def run(self): 25 | try: 26 | if self._target is not None: 27 | self._target(*self._args, **self._kwargs) 28 | except Exception as exception: 29 | thread = threading.current_thread() 30 | self.exception = exception 31 | self.logger.exception(f"{self.DIVIDER}\nException in child thread {thread}: {exception}\n{self.DIVIDER}") 32 | finally: 33 | del self._target, self._args, self._kwargs 34 | 35 | 36 | class WebsocketServerThread(ThreadWithLoggedException): 37 | """Dummy wrapper to make debug messages a bit more readable""" 38 | pass 39 | -------------------------------------------------------------------------------- /python/archived/0.1.8.1/cloudlink/websocket_server/thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class ThreadWithLoggedException(threading.Thread): 5 | """ 6 | Similar to Thread but will log exceptions to passed logger. 7 | 8 | Args: 9 | logger: Logger instance used to log any exception in child thread 10 | 11 | Exception is also reachable via .exception from the main thread. 12 | """ 13 | 14 | DIVIDER = "*"*80 15 | 16 | def __init__(self, *args, **kwargs): 17 | try: 18 | self.logger = kwargs.pop("logger") 19 | except KeyError: 20 | raise Exception("Missing 'logger' in kwargs") 21 | super().__init__(*args, **kwargs) 22 | self.exception = None 23 | 24 | def run(self): 25 | try: 26 | if self._target is not None: 27 | self._target(*self._args, **self._kwargs) 28 | except Exception as exception: 29 | thread = threading.current_thread() 30 | self.exception = exception 31 | self.logger.exception(f"{self.DIVIDER}\nException in child thread {thread}: {exception}\n{self.DIVIDER}") 32 | finally: 33 | del self._target, self._args, self._kwargs 34 | 35 | 36 | class WebsocketServerThread(ThreadWithLoggedException): 37 | """Dummy wrapper to make debug messages a bit more readable""" 38 | pass 39 | -------------------------------------------------------------------------------- /python/archived/0.1.8.2/cloudlink/websocket_server/thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class ThreadWithLoggedException(threading.Thread): 5 | """ 6 | Similar to Thread but will log exceptions to passed logger. 7 | 8 | Args: 9 | logger: Logger instance used to log any exception in child thread 10 | 11 | Exception is also reachable via .exception from the main thread. 12 | """ 13 | 14 | DIVIDER = "*"*80 15 | 16 | def __init__(self, *args, **kwargs): 17 | try: 18 | self.logger = kwargs.pop("logger") 19 | except KeyError: 20 | raise Exception("Missing 'logger' in kwargs") 21 | super().__init__(*args, **kwargs) 22 | self.exception = None 23 | 24 | def run(self): 25 | try: 26 | if self._target is not None: 27 | self._target(*self._args, **self._kwargs) 28 | except Exception as exception: 29 | thread = threading.current_thread() 30 | self.exception = exception 31 | self.logger.exception(f"{self.DIVIDER}\nException in child thread {thread}: {exception}\n{self.DIVIDER}") 32 | finally: 33 | del self._target, self._args, self._kwargs 34 | 35 | 36 | class WebsocketServerThread(ThreadWithLoggedException): 37 | """Dummy wrapper to make debug messages a bit more readable""" 38 | pass 39 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/server/websocket_server/thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class ThreadWithLoggedException(threading.Thread): 5 | """ 6 | Similar to Thread but will log exceptions to passed logger. 7 | 8 | Args: 9 | logger: Logger instance used to log any exception in child thread 10 | 11 | Exception is also reachable via .exception from the main thread. 12 | """ 13 | 14 | DIVIDER = "*"*80 15 | 16 | def __init__(self, *args, **kwargs): 17 | try: 18 | self.logger = kwargs.pop("logger") 19 | except KeyError: 20 | raise Exception("Missing 'logger' in kwargs") 21 | super().__init__(*args, **kwargs) 22 | self.exception = None 23 | 24 | def run(self): 25 | try: 26 | if self._target is not None: 27 | self._target(*self._args, **self._kwargs) 28 | except Exception as exception: 29 | thread = threading.current_thread() 30 | self.exception = exception 31 | self.logger.exception(f"{self.DIVIDER}\nException in child thread {thread}: {exception}\n{self.DIVIDER}") 32 | finally: 33 | del self._target, self._args, self._kwargs 34 | 35 | 36 | class WebsocketServerThread(ThreadWithLoggedException): 37 | """Dummy wrapper to make debug messages a bit more readable""" 38 | pass 39 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/cloudlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from .supporter import supporter 4 | 5 | """ 6 | CloudLink 4.0 Server and Client 7 | 8 | CloudLink is a free and open-source, websocket-powered API optimized for Scratch 3.0. 9 | For documentation, please visit https://hackmd.io/g6BogABhT6ux1GA2oqaOXA 10 | 11 | Server based on https://github.com/Pithikos/python-websocket-server 12 | The WebsocketServer that is bundled with CloudLink is modified to support Cloudflared and fixes an issue with asserting the HTTP websocket upgrade request. 13 | 14 | Client based upon https://github.com/websocket-client/websocket-client 15 | 16 | Please see https://github.com/MikeDev101/cloudlink for more details. 17 | """ 18 | 19 | class Cloudlink: 20 | def __init__(self): 21 | self.version = "0.1.8.3" 22 | self.supporter = supporter 23 | print(f"Cloudlink v{self.version}") 24 | 25 | def server(self, logs=False): 26 | # Initialize Cloudlink server 27 | from .server import server 28 | return server(self, logs) 29 | 30 | def client(self, logs=False): 31 | # Initialize Cloudlink client 32 | from .client import client 33 | return client(self, logs) 34 | 35 | def relay(self, logs=False): 36 | # TODO: Client and server modes now exist together, still need to finish spec and functionality for Relay mode 37 | pass 38 | -------------------------------------------------------------------------------- /golang/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "runtime/debug" 6 | "time" 7 | 8 | "github.com/gofiber/contrib/websocket" 9 | "github.com/gofiber/fiber/v2" 10 | "github.com/gofiber/fiber/v2/middleware/monitor" 11 | 12 | cloudlink "github.com/mikedev101/cloudlink/golang/server" 13 | ) 14 | 15 | func main() { 16 | // Configure runtime settings 17 | debug.SetGCPercent(35) // 35% limit for GC 18 | 19 | // Create fiber application 20 | app := fiber.New() 21 | 22 | // Create CloudLink server 23 | manager := cloudlink.New("root") 24 | 25 | // Modify configuration of the CloudLink manager 26 | manager.Config.CheckIPAddresses = false 27 | manager.Config.EnableMOTD = true 28 | manager.Config.MOTDMessage = "CloudLink Golang using Gofiber!" 29 | 30 | // Add a websocket path 31 | app.Use("/ws", func(c *fiber.Ctx) error { 32 | // IsWebSocketUpgrade returns true if the client 33 | // requested upgrade to the WebSocket protocol. 34 | if websocket.IsWebSocketUpgrade(c) { 35 | // c.Locals("allowed", true) 36 | return c.Next() 37 | } 38 | return fiber.ErrUpgradeRequired 39 | }) 40 | 41 | // Bind CloudLink server to websocket path 42 | app.Get("/ws", websocket.New(func(client *websocket.Conn) { 43 | cloudlink.SessionHandler(client, manager) 44 | })) 45 | 46 | app.Get("/monitor", monitor.New( 47 | monitor.Config{ 48 | Title: "CloudLink Metrics", 49 | Refresh: (50 * time.Millisecond), 50 | }, 51 | )) 52 | 53 | log.Fatal(app.Listen(":3000")) 54 | // Access the websocket server: ws://0.0.0.0:3000/ 55 | 56 | //log.Fatal(app.ListenTLS("0.0.0.0:3000", "./cert.pem", "./key.pem")) 57 | // Access the websocket server: wss://0.0.0.0:3000/ 58 | } 59 | -------------------------------------------------------------------------------- /python/archived/0.1.7/server_example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import CloudLink 2 | 3 | """ 4 | CloudLink v0.1.7 Server Example 5 | 6 | This code demonstrates how easy it is to connect run a server. It also shows how to write functions 7 | to interact with CloudLink. 8 | 9 | For more information, please visit https://hackmd.io/G9q1kPqvQT6NrPobjjxSgg 10 | 11 | NOTICE ABOUT CALLBACKS 12 | The on_packet callback differs in what it does when used in server 13 | mode than it does in client mode. 14 | 15 | In server mode, the on_packet callback will only return back packets 16 | sent using the "direct" command, and will return the value of "val". 17 | 18 | If a client sends {"cmd": "direct", "val": "test"}, the server will 19 | take this message and callback the on_packet callback here, only 20 | returning the value "test" as the message. 21 | 22 | In client mode, the message will return the entire packet. 23 | 24 | As shown above, if a client sends {"cmd": "test", "val": "test", "id": "(YOUR ID HERE)"} 25 | to (YOUR ID HERE), the on_packet callback will return the entire 26 | JSON as the message. 27 | """ 28 | 29 | def on_packet(message): 30 | print(message) 31 | cl.sendPacket({"cmd": "statuscode", "val": cl.codes["Test"], "id": message["id"]}) 32 | cl.sendPacket({"cmd": "direct", "val": message["val"], "id": message["id"]}) 33 | print(cl.getUsernames()) 34 | 35 | if __name__ == "__main__": 36 | cl = CloudLink(debug=True) 37 | # Instanciates a CloudLink object into memory. 38 | 39 | cl.callback("on_packet", on_packet) 40 | # Create callbacks to functions. 41 | 42 | cl.setMOTD("CloudLink test", enable=True) 43 | # Sets the server Message of the Day. 44 | 45 | cl.server() 46 | # Start CloudLink and run as a server. 47 | -------------------------------------------------------------------------------- /python/archived/0.1.9.2/cloudlink/cloudlink.py: -------------------------------------------------------------------------------- 1 | from .supporter import supporter 2 | 3 | """ 4 | CloudLink 4.0 Server and Client 5 | 6 | CloudLink is a free and open-source, websocket-powered API optimized for Scratch 3.0. 7 | For documentation, please visit https://hackmd.io/g6BogABhT6ux1GA2oqaOXA 8 | 9 | Cloudlink is built upon https://github.com/aaugustin/websockets. 10 | 11 | Please see https://github.com/MikeDev101/cloudlink for more details. 12 | 13 | Cloudlink's dependencies are: 14 | * websockets (for server and asyncio client) 15 | * websocket-client (for non-asyncio client) 16 | 17 | These dependencies are built-in to Python. 18 | * copy 19 | * asyncio 20 | * traceback 21 | * datetime 22 | * json 23 | """ 24 | 25 | 26 | class cloudlink: 27 | def __init__(self): 28 | self.version = "0.1.9.2" 29 | self.supporter = supporter 30 | 31 | def server(self, logs: bool = False): 32 | # Initialize Cloudlink server 33 | from .server import server 34 | return server(self, logs) 35 | 36 | def client(self, logs: bool = False, async_client: bool = True): 37 | # Initialize Cloudlink client 38 | if async_client: 39 | from .async_client import async_client 40 | return async_client.client(self, logs) 41 | else: 42 | from .old_client import old_client 43 | return old_client.client(self, logs) 44 | 45 | def multi_client(self, logs: bool = False, async_client: bool = True): 46 | # Initialize Cloudlink client 47 | if async_client: 48 | from .async_client import async_client 49 | return async_client.multi_client(self, logs) 50 | else: 51 | from .old_client import old_client 52 | return old_client.multi_client(self, logs) 53 | -------------------------------------------------------------------------------- /python/client_example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import client 2 | 3 | if __name__ == "__main__": 4 | # Initialize the client 5 | client = client() 6 | 7 | # Configure logging settings 8 | client.logging.basicConfig( 9 | level=client.logging.DEBUG 10 | ) 11 | 12 | # Use this decorator to handle established connections. 13 | @client.on_connect 14 | async def on_connect(): 15 | print("Connected!") 16 | 17 | # Ask for a username 18 | await client.protocol.set_username(input("Please give me a username... ")) 19 | 20 | # Whenever a client is connected, you can call this function to gracefully disconnect. 21 | # client.disconnect() 22 | 23 | # Use this decorator to handle disconnects. 24 | @client.on_disconnect 25 | async def on_disconnect(): 26 | print("Disconnected!") 27 | 28 | # Use this decorator to handle username being set events. 29 | @client.on_username_set 30 | async def on_username_set(id, name, uuid): 31 | print(f"My username has been set! ID: {id}, Name: {name}, UUID: {uuid}") 32 | 33 | # Example message-specific event handler. You can use different kinds of message types, 34 | # such as pmsg, gvar, pvar, and more. 35 | @client.on_gmsg 36 | async def on_gmsg(message): 37 | print(f"I got a global message! It says: \"{message['val']}\".") 38 | 39 | # Example use of on_command functions within the client. 40 | @client.on_command(cmd="gmsg") 41 | async def on_gmsg(message): 42 | client.send_packet({"cmd": "direct", "val": "Hello, server!"}) 43 | 44 | # Enable SSL support (if you use self-generated SSL certificates) 45 | #client.enable_ssl(certfile="cert.pem") 46 | 47 | # Start the client 48 | client.run(host="ws://127.0.0.1:3000/") 49 | -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/cloudlink.py: -------------------------------------------------------------------------------- 1 | from .supporter import supporter 2 | 3 | """ 4 | CloudLink 4.0 Server and Client 5 | 6 | CloudLink is a free and open-source, websocket-powered API optimized for Scratch 3.0. 7 | For documentation, please visit https://hackmd.io/g6BogABhT6ux1GA2oqaOXA 8 | 9 | Cloudlink is built upon https://github.com/aaugustin/websockets. 10 | 11 | Please see https://github.com/MikeDev101/cloudlink for more details. 12 | 13 | Cloudlink's dependencies are: 14 | * websockets 15 | 16 | These dependencies are built-in to Python. 17 | * copy 18 | * asyncio 19 | * traceback 20 | * datetime 21 | * json 22 | """ 23 | 24 | 25 | class cloudlink: 26 | def __init__(self): 27 | self.version = "0.1.9.1" 28 | self.supporter = supporter 29 | print(f"Cloudlink v{self.version}") 30 | 31 | def server(self, logs: bool = False): 32 | # Initialize Cloudlink server 33 | from .server import server 34 | return server(self, logs) 35 | 36 | def client(self, logs: bool = False, async_client: bool = True): 37 | # Initialize Cloudlink client 38 | if async_client: 39 | from .async_client import async_client 40 | return async_client.client(self, logs) 41 | else: 42 | from .old_client import old_client 43 | return old_client.client(self, logs) 44 | 45 | def multi_client(self, logs: bool = False, async_client: bool = True): 46 | # Initialize Cloudlink client 47 | if async_client: 48 | from .async_client import async_client 49 | return async_client.multi_client(self, logs) 50 | else: 51 | from .old_client import old_client 52 | return old_client.multi_client(self, logs) 53 | 54 | def relay(self, logs: bool = False): 55 | # TODO: Client and server modes now exist together, still need to finish spec and functionality for Relay mode 56 | pass 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CLOUDLINK 4 BANNER](https://user-images.githubusercontent.com/12957745/188282246-a221e66a-5d8a-4516-9ae2-79212b745d91.png) 2 | 3 | # PROJECT DISCONTINUED 4 | CL as it stands has been indefinitely discontinued. You are free to fork it and continue development as needed. 5 | 6 | # SUCCESSOR IN DEVELOPMENT 7 | [A far superior version of CloudLink is in development.](https://github.com/cloudlink-omega) Please consider giving it a try! 8 | 9 | # Cloudlink 10 | CloudLink is a free and open-source websocket solution optimized for Scratch. 11 | Originally created as a cloud variables alternative, it can function as a multipurpose websocket framework for other projects. 12 | 13 | # 🍨 Now in multiple flavors! 14 | See ./python for the original Python-based server. 15 | See ./golang for the WIP Go-based server. 16 | 17 | # 🐈 A powerful extension for Scratch 3.0 18 | You can learn about the protocol using the original Scratch 3.0 client extension. 19 | Feel free to test-drive the extension in any of these Scratch mods: 20 | 21 | - [PenguinMod](https://studio.penguinmod.com/editor.html?extension=https://extensions.penguinmod.com/extensions/MikeDev101/cloudlink.js) _(Click and run!)_ 22 | - [TurboWarp](https://turbowarp.org/editor) _(You'll need to import the [extension code](https://mikedev101.github.io/cloudlink/scratch/cloudlink_turbowarp.js) as a file.)_ 23 | - [SheepTester's E羊icques](https://sheeptester.github.io/scratch-gui) _(You'll need to [inject](https://chrome.google.com/webstore/detail/code-injector/edkcmfocepnifkbnbkmlcmegedeikdeb) the [extension code](https://mikedev101.github.io/cloudlink/scratch/cloudlink_epicques.js) manually.)_ 24 | 25 | _Currently not supported (I'm working on it!):_ 26 | - [Ogadaki's Adacraft](https://adacraft.org/studio/) 27 | - [Ogadaki's Adacraft (Beta)](https://beta.adacraft.org/studio/) 28 | 29 | # 📃 The CloudLink Protocol 📃 30 | Documentation of the CL4 protocol can be found in the CloudLink Repository's [Wiki page.](https://github.com/MikeDev101/cloudlink/wiki) 31 | -------------------------------------------------------------------------------- /python/archived/0.1.7/client_example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import CloudLink 2 | 3 | """ 4 | CloudLink v0.1.7 Client Example 5 | 6 | This code demonstrates how easy it is to connect to a server. It also shows how to write functions 7 | to interact with CloudLink. 8 | 9 | For more information, please visit https://hackmd.io/G9q1kPqvQT6NrPobjjxSgg 10 | """ 11 | 12 | def on_connect(): 13 | print("I am now connected!") 14 | cl.sendPacket({"cmd": "setid", "val": str(input("Enter a username... "))}) 15 | 16 | def on_error(error): 17 | print("Oops, I got an error! {0}".format(error)) 18 | 19 | def on_packet(message): 20 | print("I got a new message! {0}".format(message)) 21 | print(cl.getUsernames()) 22 | 23 | """ 24 | The on_packet callback differs in what it does when used in server 25 | mode than it does in client mode. 26 | 27 | In server mode, the on_packet callback will only return back packets 28 | sent using the "direct" command, and will return the value of "val". 29 | 30 | If a client sends {"cmd": "direct", "val": "test"}, the server will 31 | take this message and callback the on_packet callback here, only 32 | returning the value "test" as the message. 33 | 34 | In client mode, the message will return all JSON with only modifications 35 | to remove the ID (if we're sending packets to someone, they don't need extra 36 | garbage data telling that it's theirs), and adds the "origin" value to tell 37 | the client who the packet was sent from. 38 | 39 | As shown above, if a client sends {"cmd": "test", "val": "test", "id": "(YOUR ID HERE)"} 40 | to (YOUR ID HERE), the on_packet callback will return 41 | {"cmd" "test", "val": "test", "origin" "(CLIENT'S ID)"} 42 | """ 43 | 44 | def on_close(): 45 | print("Goodbye!") 46 | 47 | if __name__ == "__main__": 48 | cl = CloudLink(debug=True) 49 | # Instanciates a CloudLink object into memory. 50 | 51 | cl.callback("on_packet", on_packet) 52 | cl.callback("on_error", on_error) 53 | cl.callback("on_connect", on_connect) 54 | cl.callback("on_close", on_close) 55 | # Create callbacks to functions. 56 | 57 | cl.client() 58 | # Start CloudLink and run as a client. -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # CloudLink Python 2 | This is the original, Python-based codebase for CloudLink server. 3 | 4 | ## 💡 Features 💡 5 | 6 | ### 🪶 Fast and lightweight 7 | CloudLink can run on minimal resources. At least 25MB of RAM and any reasonably capable CPU can run a CloudLink server. 8 | 9 | ### 🌐 Essential networking tools 10 | * Unicast and multicast packets across clients 11 | * Expandable functionality with a built-in method loader 12 | 13 | ### 📦 Minimal dependencies 14 | All dependencies below can be installed using `pip install -r requirements.txt`. 15 | * 🐍 Python >=3.11 16 | * 🧵 asyncio (Built-in) 17 | * 📃 ["ujson" ultrajson](https://github.com/ultrajson/ultrajson) 18 | * 🔍 [pyeve/cerberus](https://github.com/pyeve/cerberus) 19 | * ❄️ ["snowflake-id" vd2org/snowflake](https://github.com/vd2org/snowflake) 20 | * 🌐 [aaugustin/websockets](https://github.com/aaugustin/websockets) 21 | 22 | ### 🔋Batteries included 23 | The CloudLink Python server comes with full support for the CL4 protocol and the Scratch cloud variable protocol. 24 | Just download, setup, and start! 25 | 26 | ### 🧱 Plug-and-play modularity 27 | You can easily extend the functionality of the server using classes and decorators. 28 | Here's an example of a simple plugin that displays "Foobar!" in the console 29 | when a client sends the message `{ "cmd": "foo" }` to the server. 30 | 31 | ```python 32 | # Import the server 33 | from cloudlink import server 34 | 35 | # Import default protocol 36 | from cloudlink.server.protocols import clpv4 37 | 38 | # Instantiate the server object 39 | server = server() 40 | 41 | # Set logging level 42 | server.logging.basicConfig( 43 | level=server.logging.DEBUG 44 | ) 45 | 46 | # Load default CL protocol 47 | clpv4 = clpv4(server) 48 | 49 | # Define the functions your plugin executes 50 | class myplugin: 51 | def __init__(self, server, protocol): 52 | 53 | # Example command - client sends { "cmd": "foo" } to the server, this function will execute 54 | @server.on_command(cmd="foo", schema=protocol.schema) 55 | async def foobar(client, message): 56 | print("Foobar!") 57 | 58 | # Load the plugin! 59 | myplugin(server, clpv4) 60 | 61 | # Start the server! 62 | server.run() 63 | ``` 64 | -------------------------------------------------------------------------------- /python/cloudlink/server/protocols/scratch/schema.py: -------------------------------------------------------------------------------- 1 | # Schema for interpreting the Cloud Variables protocol used in Scratch 3.0 2 | class scratch_protocol: 3 | 4 | # Required - Defines the keyword to use to define the command 5 | command_key = "method" 6 | 7 | # Required - Defines the default schema to test against 8 | default = { 9 | "method": { 10 | "type": "string", 11 | "required": True 12 | }, 13 | "name": { 14 | "type": "string", 15 | "required": False 16 | }, 17 | "new_name": { 18 | "type": "string", 19 | "required": False 20 | }, 21 | "value": { 22 | "type": [ 23 | "string", 24 | "integer", 25 | "float", 26 | "boolean", 27 | "dict", 28 | "list", 29 | "set" 30 | ], 31 | "required": False 32 | }, 33 | "project_id": { 34 | "type": [ 35 | "string", 36 | "integer", 37 | "float", 38 | "boolean" 39 | ], 40 | "required": False, 41 | }, 42 | "user": { 43 | "type": "string", 44 | "required": False, 45 | "minlength": 1, 46 | "maxlength": 20 47 | } 48 | } 49 | 50 | handshake = { 51 | "method": { 52 | "type": "string", 53 | "required": True 54 | }, 55 | "project_id": { 56 | "type": [ 57 | "string", 58 | "integer", 59 | "float", 60 | "boolean" 61 | ], 62 | "required": True, 63 | }, 64 | "user": { 65 | "type": "string", 66 | "required": True, 67 | } 68 | } 69 | 70 | method = { 71 | "method": { 72 | "type": "string", 73 | "required": True 74 | }, 75 | "name": { 76 | "type": "string", 77 | "required": True 78 | }, 79 | "new_name": { 80 | "type": "string", 81 | "required": False 82 | }, 83 | "value": { 84 | "type": [ 85 | "string", 86 | "integer", 87 | "float", 88 | "boolean", 89 | "dict", 90 | "list", 91 | "set" 92 | ], 93 | "required": False 94 | }, 95 | "project_id": { 96 | "type": [ 97 | "string", 98 | "integer", 99 | "float", 100 | "boolean" 101 | ], 102 | "required": True, 103 | }, 104 | "user": { 105 | "type": "string", 106 | "required": False, 107 | "minlength": 1, 108 | "maxlength": 20 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /python/archived/0.1.8.1/example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import Cloudlink 2 | 3 | class customCommands: 4 | """ 5 | customCommands 6 | 7 | This is an example of Cloudlink 4.0's new custom commands system. 8 | """ 9 | 10 | def __init__(self, cloudlink): 11 | # To use custom commands, you will need to initialize your custom commands class with Cloudlink. This is required. 12 | self.cloudlink = cloudlink 13 | # You can specify which functions to ignore when using Cloudlink.server.loadCustomCommands. This is optional. 14 | self.importer_ignore_functions = [] # ["test"] if you don't want to load the custom command "test". 15 | 16 | # Optionally, you can reference Cloudlink components for extended functionality. 17 | self.supporter = self.cloudlink.supporter 18 | 19 | def test(self, client, server, message, listener_detected, listener_id, room_id): 20 | """ 21 | To define a custom command, a command must contain the following parameters: 22 | self, client, server, message, listener_detected, listener_id, room_id 23 | 24 | self - Should be a class reference to itself. 25 | client - Dictionary object that identifies clients. Contains headers, address, handler, and id. See /cloudlink/websocket_server/websocket_server.py for info. 26 | server - Required for the websocket server and for backward compatibility. 27 | listener_detected - Boolean that is set when Cloudlink.server.serverRootHandlers checks a packet and identifies the JSON key "listener" in a packet. 28 | listener_id - Any value that is set when Cloudlink.server.serverRootHandlers checks a packet and reads the JSON key "listener" in a packet. 29 | room_id - Array of strings that is set when a client has been linked to rooms. Defaults to either None or ["default"]. 30 | 31 | Most of the time, you will be passing listener_detected, listener_id, and room_id to various cloudlink functions. You will likely never use server. 32 | """ 33 | self.cloudlink.sendPacket(client, {"cmd": "direct", "val": "test"}, listener_detected, listener_id, room_id) 34 | 35 | class demoCallbacks: 36 | """ 37 | demoCallbacks 38 | 39 | This is an example of Cloudlink's callback compatibility system, which re-implements callbacks used in older versions of Cloudlink. 40 | It is not recommended to use this feature in newer implementations as it has been replaced with Cloudlink.server.loadCustomCommands. 41 | """ 42 | 43 | def __init__(self): 44 | pass 45 | 46 | def on_packet(self, client, server, message): 47 | print("on_packet") 48 | 49 | def on_connect(self, client, server): 50 | print("on_connect") 51 | 52 | def on_close(self, client, server): 53 | print("on_close") 54 | 55 | if __name__ == "__main__": 56 | cl = Cloudlink() 57 | dummy = demoCallbacks() 58 | 59 | server = cl.server(logs=True) 60 | server.setMOTD(True, "CloudLink 4 Test") 61 | 62 | server.callback(server.on_packet, dummy.on_packet) 63 | server.callback(server.on_connect, dummy.on_connect) 64 | server.callback(server.on_close, dummy.on_close) 65 | 66 | server.loadCustomCommands(customCommands) 67 | #server.disableCommands(["gmsg"]) 68 | 69 | server.run(host="0.0.0.0", port=3000) -------------------------------------------------------------------------------- /python/archived/0.1.8.2/example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import Cloudlink 2 | 3 | class customCommands: 4 | """ 5 | customCommands 6 | 7 | This is an example of Cloudlink 4.0's new custom commands system. 8 | """ 9 | 10 | def __init__(self, cloudlink): 11 | # To use custom commands, you will need to initialize your custom commands class with Cloudlink. This is required. 12 | self.cloudlink = cloudlink 13 | # You can specify which functions to ignore when using Cloudlink.server.loadCustomCommands. This is optional. 14 | self.importer_ignore_functions = [] # ["test"] if you don't want to load the custom command "test". 15 | 16 | # Optionally, you can reference Cloudlink components for extended functionality. 17 | self.supporter = self.cloudlink.supporter 18 | 19 | def test(self, client, server, message, listener_detected, listener_id, room_id): 20 | """ 21 | To define a custom command, a command must contain the following parameters: 22 | self, client, server, message, listener_detected, listener_id, room_id 23 | 24 | self - Should be a class reference to itself. 25 | client - Dictionary object that identifies clients. Contains headers, address, handler, and id. See /cloudlink/websocket_server/websocket_server.py for info. 26 | server - Required for the websocket server and for backward compatibility. 27 | listener_detected - Boolean that is set when Cloudlink.server.serverRootHandlers checks a packet and identifies the JSON key "listener" in a packet. 28 | listener_id - Any value that is set when Cloudlink.server.serverRootHandlers checks a packet and reads the JSON key "listener" in a packet. 29 | room_id - Array of strings that is set when a client has been linked to rooms. Defaults to either None or ["default"]. 30 | 31 | Most of the time, you will be passing listener_detected, listener_id, and room_id to various cloudlink functions. You will likely never use server. 32 | """ 33 | self.cloudlink.sendPacket(client, {"cmd": "direct", "val": "test"}, listener_detected, listener_id, room_id) 34 | 35 | class demoCallbacks: 36 | """ 37 | demoCallbacks 38 | 39 | This is an example of Cloudlink's callback compatibility system, which re-implements callbacks used in older versions of Cloudlink. 40 | It is not recommended to use this feature in newer implementations as it has been replaced with Cloudlink.server.loadCustomCommands. 41 | """ 42 | 43 | def __init__(self): 44 | pass 45 | 46 | def on_packet(self, client, server, message): 47 | print("on_packet") 48 | 49 | def on_connect(self, client, server): 50 | print("on_connect") 51 | 52 | def on_close(self, client, server): 53 | print("on_close") 54 | 55 | if __name__ == "__main__": 56 | cl = Cloudlink() 57 | dummy = demoCallbacks() 58 | 59 | server = cl.server(logs=True) 60 | server.setMOTD(True, "CloudLink 4 Test") 61 | 62 | server.callback(server.on_packet, dummy.on_packet) 63 | server.callback(server.on_connect, dummy.on_connect) 64 | server.callback(server.on_close, dummy.on_close) 65 | 66 | server.loadCustomCommands(customCommands) 67 | #server.disableCommands(["gmsg"]) 68 | 69 | server.run(host="0.0.0.0", port=3000) -------------------------------------------------------------------------------- /python/server_example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import server 2 | from cloudlink.server.protocols import clpv4, scratch 3 | import asyncio 4 | 5 | 6 | class example_callbacks: 7 | def __init__(self, parent): 8 | self.parent = parent 9 | 10 | async def test1(self, client, message): 11 | print("Test1!") 12 | await asyncio.sleep(1) 13 | print("Test1 after one second!") 14 | 15 | async def test2(self, client, message): 16 | print("Test2!") 17 | await asyncio.sleep(1) 18 | print("Test2 after one second!") 19 | 20 | async def test3(self, client, message): 21 | print("Test3!") 22 | 23 | 24 | class example_commands: 25 | def __init__(self, parent, protocol): 26 | 27 | # Creating custom commands - This example adds a custom command called "foobar". 28 | @server.on_command(cmd="foobar", schema=protocol.schema) 29 | async def foobar(client, message): 30 | print("Foobar!") 31 | 32 | # Reading the IP address of the client is as easy as calling get_client_ip from the clpv4 protocol object. 33 | print(protocol.get_client_ip(client)) 34 | 35 | # In case you need to report a status code, use send_statuscode. 36 | protocol.send_statuscode( 37 | client=client, 38 | code=protocol.statuscodes.ok, 39 | message=message 40 | ) 41 | 42 | 43 | class example_events: 44 | def __init__(self): 45 | pass 46 | 47 | async def on_close(self, client): 48 | print("Client", client.id, "disconnected.") 49 | 50 | async def on_connect(self, client): 51 | print("Client", client.id, "connected.") 52 | 53 | 54 | if __name__ == "__main__": 55 | # Initialize the server 56 | server = server() 57 | 58 | # Configure logging settings 59 | server.logging.basicConfig( 60 | level=server.logging.DEBUG 61 | ) 62 | 63 | # Load protocols 64 | clpv4 = clpv4(server) 65 | scratch = scratch(server) 66 | 67 | # Load examples 68 | callbacks = example_callbacks(server) 69 | commands = example_commands(server, clpv4) 70 | events = example_events() 71 | 72 | # Binding callbacks - This example binds the "handshake" command with example callbacks. 73 | # You can bind as many functions as you want to a callback, but they must use async. 74 | # To bind callbacks to built-in methods (example: gmsg), see cloudlink.cl_methods. 75 | server.bind_callback(cmd="handshake", schema=clpv4.schema, method=callbacks.test1) 76 | server.bind_callback(cmd="handshake", schema=clpv4.schema, method=callbacks.test2) 77 | 78 | # Binding events - This example will print a client connect/disconnect message. 79 | # You can bind as many functions as you want to an event, but they must use async. 80 | # To see all possible events for the server, see cloudlink.events. 81 | server.bind_event(server.on_connect, events.on_connect) 82 | server.bind_event(server.on_disconnect, events.on_close) 83 | 84 | # You can also bind an event to a custom command. We'll bind callbacks.test3 to our 85 | # foobar command from earlier. 86 | server.bind_callback(cmd="foobar", schema=clpv4.schema, method=callbacks.test3) 87 | 88 | # Initialize SSL support 89 | # server.enable_ssl(certfile="cert.pem", keyfile="privkey.pem") 90 | 91 | # Start the server 92 | server.run(ip="127.0.0.1", port=3000) 93 | -------------------------------------------------------------------------------- /python/archived/0.1.8.0/cloudlink/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from websocket_server import WebsocketServer 3 | from supporter import supporter 4 | from serverRootHandlers import serverRootHandlers 5 | from serverInternalHandlers import serverInternalHandlers 6 | import logging 7 | 8 | class server: 9 | def __init__(self, parentCl, enable_logs): 10 | # Read the CloudLink version from the parent class 11 | self.version = parentCl.version 12 | 13 | # Init stuff for the server 14 | self.userlist = [] 15 | self.motd_enable = False 16 | self.motd_msg = "" 17 | self.global_msg = "" 18 | 19 | # Init modules 20 | self.supporter = supporter(self, enable_logs) 21 | self.log = self.supporter.log 22 | self.serverRootHandlers = serverRootHandlers(self) 23 | self.serverInternalHandlers = serverInternalHandlers(self) 24 | 25 | # Load built-in commands (automatically generates attributes for callbacks) 26 | self.builtInCommands = [] 27 | self.customCommands = [] 28 | self.disabledCommands = [] 29 | self.usercallbacks = {} 30 | self.supporter.loadBuiltinCommands() 31 | 32 | # Extra configuration 33 | self.ipblocklist = [] # Use to block IP addresses 34 | self.rejectClientMode = False # Set to true to reject future clients until false 35 | 36 | # Create API for friendlier callbacks 37 | self.loadCustomCommands = self.supporter.loadCustomCommands 38 | self.disableCommands = self.supporter.disableCommands 39 | self.sendPacket = self.supporter.sendPacket 40 | self.sendCode = self.supporter.sendCode 41 | self.on_packet = self.serverRootHandlers.on_packet 42 | self.on_connect = self.serverRootHandlers.on_connect 43 | self.on_close = self.serverRootHandlers.on_close 44 | 45 | def run(self, port=3000, host="127.0.0.1"): 46 | # Initialize the Websocket Server 47 | self.log("Cloudlink server starting up now...") 48 | self.wss = WebsocketServer(host, port) 49 | 50 | # Bind built-in callbacks 51 | self.wss.set_fn_new_client(self.serverRootHandlers.on_connect) 52 | self.wss.set_fn_message_received(self.serverRootHandlers.on_packet) 53 | self.wss.set_fn_client_left(self.serverRootHandlers.on_close) 54 | 55 | # Create attributes 56 | self.all_clients = self.wss.clients 57 | 58 | # Run the CloudLink server 59 | self.wss.run_forever() 60 | self.log("Cloudlink server exiting...") 61 | 62 | def setMOTD(self, enable:bool, msg:str): 63 | self.motd_enable = enable 64 | self.motd_msg = msg 65 | 66 | def callback(self, callback_id, function): 67 | # Support older servers which use the old callback system. 68 | if type(callback_id) == str: 69 | callback_id = getattr(self, callback_id) 70 | 71 | # New callback system. 72 | if callable(callback_id): 73 | if callback_id == self.on_packet: 74 | self.usercallbacks[self.on_packet] = function 75 | elif callback_id == self.on_connect: 76 | self.usercallbacks[self.on_connect] = function 77 | elif callback_id == self.on_close: 78 | self.usercallbacks[self.on_close] = function 79 | elif callback_id == self.on_error: 80 | self.usercallbacks[self.on_error] = function -------------------------------------------------------------------------------- /golang/server/sessions.go: -------------------------------------------------------------------------------- 1 | package cloudlink 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/goccy/go-json" 7 | 8 | "github.com/gofiber/contrib/websocket" 9 | ) 10 | 11 | func (client *Client) CloseWithMessage(statuscode int, closeMessage string) { 12 | client.connection.WriteMessage( 13 | websocket.CloseMessage, 14 | websocket.FormatCloseMessage( 15 | statuscode, 16 | closeMessage, 17 | ), 18 | ) 19 | client.connection.Close() 20 | } 21 | 22 | func (client *Client) MessageHandler(manager *Manager) { 23 | // websocket.Conn bindings https://pkg.go.dev/github.com/fasthttp/websocket?tab=doc#pkg-index 24 | var ( 25 | _ int 26 | message []byte 27 | err error 28 | ) 29 | for { 30 | // Listen for new messages 31 | if _, message, err = client.connection.ReadMessage(); err != nil { 32 | break 33 | } 34 | 35 | switch client.protocol { 36 | case 0: // Detect protocol 37 | // CL4 protocol 38 | var cl4packet PacketUPL 39 | if err := json.Unmarshal([]byte(message), &cl4packet); err != nil { 40 | client.CloseWithMessage(websocket.CloseUnsupportedData, "JSON parsing error") 41 | } 42 | 43 | // Scratch protocol 44 | var scratchpacket Scratch 45 | if err := json.Unmarshal([]byte(message), &scratchpacket); err != nil { 46 | client.CloseWithMessage(websocket.CloseUnsupportedData, "JSON parsing error") 47 | } 48 | 49 | // Handle requests 50 | if cl4packet.Cmd != "" { 51 | CL4ProtocolDetect(client) 52 | CL4MethodHandler(client, &cl4packet) 53 | } else if scratchpacket.Method != "" { 54 | ScratchProtocolDetect(client) 55 | ScratchMethodHandler(client, &scratchpacket) 56 | } else { 57 | client.CloseWithMessage(websocket.CloseProtocolError, "Couldn't identify protocol") 58 | } 59 | 60 | case 1: // CL4 61 | var cl4packet PacketUPL 62 | if err := json.Unmarshal([]byte(message), &cl4packet); err != nil { 63 | client.CloseWithMessage(websocket.CloseUnsupportedData, "JSON parsing error") 64 | } 65 | CL4MethodHandler(client, &cl4packet) 66 | 67 | case 2: // Scratch 68 | var scratchpacket Scratch 69 | if err := json.Unmarshal([]byte(message), &scratchpacket); err != nil { 70 | client.CloseWithMessage(websocket.CloseUnsupportedData, "JSON parsing error") 71 | } 72 | ScratchMethodHandler(client, &scratchpacket) 73 | } 74 | } 75 | } 76 | 77 | // SessionHandler is the root function that makes CloudLink work. As soon as a client request gets upgraded to the websocket protocol, this function should be called. 78 | func SessionHandler(con *websocket.Conn, manager *Manager) { 79 | /* 80 | // con.Locals is added to the *websocket.Conn 81 | log.Println(con.Locals("allowed")) // true 82 | log.Println(con.Params("id")) // 123 83 | log.Println(con.Query("v")) // 1.0 84 | log.Println(con.Cookies("session")) // "" 85 | */ 86 | 87 | // Register client 88 | client := NewClient(con, manager) 89 | manager.AddClient(client) 90 | 91 | // Log IP address of client (if enabled) 92 | if manager.Config.CheckIPAddresses { 93 | log.Printf("[%s] Client %s (%s) IP address: %s", manager.name, client.id, client.uuid, con.RemoteAddr().String()) 94 | } 95 | 96 | // Remove client from manager once the session has ended 97 | defer manager.RemoveClient(client) 98 | 99 | // Begin handling messages throughout the lifespan of the connection 100 | client.MessageHandler(manager) 101 | } 102 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/server/server.py: -------------------------------------------------------------------------------- 1 | from .websocket_server import WebsocketServer 2 | from .serverRootHandlers import serverRootHandlers 3 | from .serverInternalHandlers import serverInternalHandlers 4 | 5 | class server: 6 | def __init__(self, parentCl, enable_logs=True): 7 | # Read the CloudLink version from the parent class 8 | self.version = parentCl.version 9 | 10 | # Init the server 11 | self.userlist = [] 12 | self.motd_enable = False 13 | self.motd_msg = "" 14 | self.global_msg = "" 15 | self.roomData = { 16 | "default": set() 17 | } 18 | 19 | # Init modules 20 | self.supporter = parentCl.supporter(self, enable_logs, 1) 21 | self.serverRootHandlers = serverRootHandlers(self) 22 | self.serverInternalHandlers = serverInternalHandlers(self) 23 | 24 | # Load built-in commands (automatically generates attributes for callbacks) 25 | self.builtInCommands = [] 26 | self.customCommands = [] 27 | self.disabledCommands = [] 28 | self.usercallbacks = {} 29 | self.supporter.loadBuiltinCommands(self.serverInternalHandlers) 30 | 31 | # Extra configuration 32 | self.ipblocklist = [] # Use to block IP addresses 33 | self.rejectClientMode = False # Set to true to reject future clients until false 34 | 35 | # Create API 36 | self.loadCustomCommands = self.supporter.loadCustomCommands 37 | self.disableCommands = self.supporter.disableCommands 38 | self.sendPacket = self.supporter.sendPacket 39 | self.sendCode = self.supporter.sendCode 40 | self.linkClientToRooms = self.supporter.linkClientToRooms 41 | self.unlinkClientFromRooms = self.supporter.unlinkClientFromRooms 42 | self.getAllUsersInRoom = self.supporter.getAllUsersInRoom 43 | self.getAllUsersInManyRooms = self.supporter.getAllUsersInManyRooms 44 | self.getAllRooms = self.supporter.getAllRooms 45 | self.getAllClientRooms = self.supporter.getAllClientRooms 46 | self.getUsernames = self.supporter.getUsernames 47 | self.setClientUsername = self.supporter.setClientUsername 48 | self.log = self.supporter.log 49 | self.callback = self.supporter.callback 50 | 51 | # Create callbacks, command-specific callbacks are not needed in server mode 52 | self.on_packet = self.serverRootHandlers.on_packet 53 | self.on_connect = self.serverRootHandlers.on_connect 54 | self.on_close = self.serverRootHandlers.on_close 55 | 56 | self.log("Cloudlink server initialized!") 57 | 58 | def run(self, port=3000, host="127.0.0.1"): 59 | # Initialize the Websocket Server 60 | self.log("Cloudlink server starting up now...") 61 | self.wss = WebsocketServer(host, port) 62 | 63 | # Bind built-in callbacks 64 | self.wss.set_fn_new_client(self.serverRootHandlers.on_connect) 65 | self.wss.set_fn_message_received(self.serverRootHandlers.on_packet) 66 | self.wss.set_fn_client_left(self.serverRootHandlers.on_close) 67 | 68 | # Create attributes 69 | self.all_clients = self.wss.clients 70 | 71 | # Run the CloudLink server 72 | self.wss.run_forever() 73 | self.log("Cloudlink server exiting...") 74 | 75 | def setMOTD(self, enable:bool, msg:str): 76 | self.motd_enable = enable 77 | self.motd_msg = msg 78 | 79 | 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | ## Clients 3 | It is recommended to keep your copy of CloudLink up-to-date for improved security, performance, and stability. 4 | ### Supported Versions 5 | #### Scratch 6 | | Version number | Supported? | Note | 7 | |-------------------|------------|----------------------------------------------------------------------| 8 | | 0.1.0 | 🟢 Yes | Latest version. | 9 | | S4.1 | 🟡 Partial | Legacy support. | 10 | | S4.0 | 🟡 Partial | Legacy support. | 11 | | B3.0 | 🔴 No | End of life. | 12 | | S2.2 | 🔴 No | End of life. | 13 | | S2.1 | 🔴 No | End of life. | 14 | | S2.0 | 🔴 No | End of life. | 15 | | S1.1 | 🔴 No | End of life. | 16 | | S1.0 | 🔴 No | End of life. | 17 | 18 | #### *Notice for Meower Variants of CloudLink* 19 | Special exceptions are given to Meower-specific builds of CloudLink. They will be maintained as long as 20 | Meower supports APIv0 or the CloudLink protocol. 21 | 22 | ## Servers 23 | You are to keep your instance of Cloudlink as up-to-date as possible. You are to assume that support can be discontinued at any time, with or without reason. 24 | ### Supported Versions 25 | #### Python 26 | | Version number | Supported? | Note | 27 | |-------------------|------------|----------------------------------------------------------------------| 28 | | 0.2.0 | 🟢 Yes | Latest version. | 29 | | 0.1.9.x | 🔴 No | CL4 Optimized. EOL. | 30 | | 0.1.8.x | 🔴 No | Pre-CL4 optimized. EOL. | 31 | | 0.1.7.x and older | 🔴 No | CL3/CL Legacy - EOL. | 32 | 33 | #### *Notice for public server providers* 34 | Public server hosts should maintain the latest version. If a public server host has been found to be running on a Deprecated release, or a version that has not been upgraded in over 30 days, your public host will be removed from the public server list and you will be notified to update your server. 35 | 36 | ### Reporting vulnerabilities 37 | In the event that a vulnerability has been found, please use the following format to report said vulnerability: 38 | 39 | 1. A title of the vulnerability - Should be less than 20 words 40 | 2. A description of the vulnerability - Describe the vulnerability in as much detail as possible. Should be no larger than two paragraphs. 41 | 3. Steps to reproduce the vulnerability - Brevity is desired. 42 | 4. Scope of the vulnerability - Does the vulnerability allow a hacker to gain access to a remote machine? Does the vulnerability compromise users in any way, shape, or form? 43 | 5. Initial severity assessment - Range from Low, Medium, High or Critical 44 | 6. Any other comments/concerns 45 | -------------------------------------------------------------------------------- /python/archived/0.1.9.1/client-test-async.py: -------------------------------------------------------------------------------- 1 | from cloudlink import cloudlink 2 | 3 | 4 | class example_events: 5 | def __init__(self): 6 | pass 7 | 8 | async def on_connect(self, client): 9 | print(f"Client {client.obj_id} connected") 10 | await client.set_username(str(client.obj_id)) 11 | await client.send_gmsg("test") 12 | 13 | async def on_close(self, client): 14 | print(f"Client {client.obj_id} disconnected") 15 | 16 | async def username_set(self, client): 17 | print(f"Client {client.obj_id}'s username was set!") 18 | 19 | async def on_gmsg(self, client, message, listener): 20 | print(f"Client {client.obj_id} got gmsg {message['val']}") 21 | 22 | 23 | if __name__ == "__main__": 24 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 25 | cl = cloudlink() 26 | 27 | # Create examples for various ways to extend the functionality of Cloudlink Server. 28 | example = example_events() 29 | 30 | # Example - Multiple clients. 31 | multi_client = cl.multi_client(async_client=True, logs=True) 32 | 33 | # Spawns 5 clients. 34 | for x in range(5): 35 | # Create a new client object. This supports initializing many clients at once. 36 | client = multi_client.spawn(x, "ws://127.0.0.1:3000/") 37 | 38 | # Binding events - This example binds functions to certain events 39 | 40 | # When a client connects, all functions bound to this event will fire. 41 | client.bind_event( 42 | client.events.on_connect, 43 | example.on_connect 44 | ) 45 | 46 | # When a client disconnects, all functions bound to this event will fire. 47 | client.bind_event( 48 | client.events.on_close, 49 | example.on_close 50 | ) 51 | 52 | # When a client disconnects, all functions bound to this event will fire. 53 | client.bind_event( 54 | client.events.on_username_set, 55 | example.username_set 56 | ) 57 | 58 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 59 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 60 | 61 | print("Waking up now") 62 | multi_client.run() 63 | input("All clients are ready. Press enter to shutdown.") 64 | multi_client.stop() 65 | input("All clients have shut down. Press enter to exit.") 66 | 67 | # Example - Singular clients. 68 | 69 | # Create a new client object. 70 | client = cl.client(async_client=True, logs=True) 71 | client.obj_id = "Test" 72 | 73 | # Binding events - This example binds functions to certain events 74 | 75 | # When a client connects, all functions bound to this event will fire. 76 | client.bind_event( 77 | client.events.on_connect, 78 | example.on_connect 79 | ) 80 | 81 | # When a client disconnects, all functions bound to this event will fire. 82 | client.bind_event( 83 | client.events.on_close, 84 | example.on_close 85 | ) 86 | 87 | # When a client disconnects, all functions bound to this event will fire. 88 | client.bind_event( 89 | client.events.on_username_set, 90 | example.username_set 91 | ) 92 | 93 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 94 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 95 | 96 | # Run the client. 97 | client.run("ws://127.0.0.1:3000/") 98 | -------------------------------------------------------------------------------- /python/archived/0.1.9.1/client-test-old.py: -------------------------------------------------------------------------------- 1 | from cloudlink import cloudlink 2 | 3 | 4 | class example_events: 5 | def __init__(self): 6 | pass 7 | 8 | def on_connect(self, client): 9 | print(f"Client {client.obj_id} connected") 10 | client.set_username(str(client.obj_id)) 11 | client.send_gmsg("test") 12 | 13 | def on_close(self, client): 14 | print(f"Client {client.obj_id} disconnected") 15 | 16 | def username_set(self, client): 17 | print(f"Client {client.obj_id}'s username was set!") 18 | 19 | def on_gmsg(self, client, message, listener): 20 | print(f"Client {client.obj_id} got gmsg {message['val']}") 21 | 22 | 23 | if __name__ == "__main__": 24 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 25 | cl = cloudlink() 26 | 27 | # Create examples for various ways to extend the functionality of Cloudlink Server. 28 | example = example_events() 29 | 30 | # Example - Multiple clients. 31 | multi_client = cl.multi_client(async_client=False, logs=True) 32 | 33 | # Spawns 5 clients. 34 | for x in range(5): 35 | # Create a new client object. This supports initializing many clients at once. 36 | client = multi_client.spawn(x, "ws://127.0.0.1:3000/") 37 | 38 | # Binding events - This example binds functions to certain events 39 | 40 | # When a client connects, all functions bound to this event will fire. 41 | client.bind_event( 42 | client.events.on_connect, 43 | example.on_connect 44 | ) 45 | 46 | # When a client disconnects, all functions bound to this event will fire. 47 | client.bind_event( 48 | client.events.on_close, 49 | example.on_close 50 | ) 51 | 52 | # When a client disconnects, all functions bound to this event will fire. 53 | client.bind_event( 54 | client.events.on_username_set, 55 | example.username_set 56 | ) 57 | 58 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 59 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 60 | 61 | print("Waking up now") 62 | multi_client.run() 63 | input("All clients are ready. Press enter to shutdown.") 64 | multi_client.stop() 65 | input("All clients have shut down. Press enter to exit.") 66 | 67 | # Example - Singular clients. 68 | client = cl.client(async_client=False, logs=True) 69 | 70 | # Object IDs - Sets a friendly name to a specific client object. 71 | client.obj_id = "Test" 72 | 73 | # Binding events - This example binds functions to certain events 74 | 75 | # When a client connects, all functions bound to this event will fire. 76 | client.bind_event( 77 | client.events.on_connect, 78 | example.on_connect 79 | ) 80 | 81 | # When a client disconnects, all functions bound to this event will fire. 82 | client.bind_event( 83 | client.events.on_close, 84 | example.on_close 85 | ) 86 | 87 | # When a client disconnects, all functions bound to this event will fire. 88 | client.bind_event( 89 | client.events.on_username_set, 90 | example.username_set 91 | ) 92 | 93 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 94 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 95 | 96 | # Run the client. 97 | client.run("ws://127.0.0.1:3000/") 98 | -------------------------------------------------------------------------------- /python/archived/0.1.9.2/client-test-async.py: -------------------------------------------------------------------------------- 1 | from cloudlink import cloudlink 2 | 3 | 4 | class example_events: 5 | def __init__(self): 6 | pass 7 | 8 | async def on_connect(self, client): 9 | print(f"Client {client.obj_id} connected") 10 | await client.set_username(str(client.obj_id)) 11 | await client.send_gmsg("test") 12 | 13 | async def on_close(self, client): 14 | print(f"Client {client.obj_id} disconnected") 15 | 16 | async def username_set(self, client): 17 | print(f"Client {client.obj_id}'s username was set!") 18 | 19 | async def on_gmsg(self, client, message, listener): 20 | print(f"Client {client.obj_id} got gmsg {message['val']}") 21 | 22 | 23 | if __name__ == "__main__": 24 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 25 | cl = cloudlink() 26 | 27 | # Create examples for various ways to extend the functionality of Cloudlink Server. 28 | example = example_events() 29 | 30 | # Example - Multiple clients. 31 | multi_client = cl.multi_client(async_client=True, logs=True) 32 | 33 | # Spawns 5 clients. 34 | for x in range(5): 35 | # Create a new client object. This supports initializing many clients at once. 36 | client = multi_client.spawn(x, "ws://127.0.0.1:3000/") 37 | 38 | # Binding events - This example binds functions to certain events 39 | 40 | # When a client connects, all functions bound to this event will fire. 41 | client.bind_event( 42 | client.events.on_connect, 43 | example.on_connect 44 | ) 45 | 46 | # When a client disconnects, all functions bound to this event will fire. 47 | client.bind_event( 48 | client.events.on_close, 49 | example.on_close 50 | ) 51 | 52 | # When a client disconnects, all functions bound to this event will fire. 53 | client.bind_event( 54 | client.events.on_username_set, 55 | example.username_set 56 | ) 57 | 58 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 59 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 60 | 61 | print("Waking up now") 62 | multi_client.run() 63 | input("All clients are ready. Press enter to shutdown.") 64 | multi_client.stop() 65 | input("All clients have shut down. Press enter to exit.") 66 | 67 | # Example - Singular clients. 68 | 69 | # Create a new client object. 70 | client = cl.client(async_client=True, logs=True) 71 | client.obj_id = "Test" 72 | 73 | # Binding events - This example binds functions to certain events 74 | 75 | # When a client connects, all functions bound to this event will fire. 76 | client.bind_event( 77 | client.events.on_connect, 78 | example.on_connect 79 | ) 80 | 81 | # When a client disconnects, all functions bound to this event will fire. 82 | client.bind_event( 83 | client.events.on_close, 84 | example.on_close 85 | ) 86 | 87 | # When a client disconnects, all functions bound to this event will fire. 88 | client.bind_event( 89 | client.events.on_username_set, 90 | example.username_set 91 | ) 92 | 93 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 94 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 95 | 96 | # Run the client. 97 | client.run("ws://127.0.0.1:3000/") 98 | -------------------------------------------------------------------------------- /python/archived/0.1.9.2/client-test-old.py: -------------------------------------------------------------------------------- 1 | from cloudlink import cloudlink 2 | 3 | 4 | class example_events: 5 | def __init__(self): 6 | pass 7 | 8 | def on_connect(self, client): 9 | print(f"Client {client.obj_id} connected") 10 | client.set_username(str(client.obj_id)) 11 | client.send_gmsg("test") 12 | 13 | def on_close(self, client): 14 | print(f"Client {client.obj_id} disconnected") 15 | 16 | def username_set(self, client): 17 | print(f"Client {client.obj_id}'s username was set!") 18 | 19 | def on_gmsg(self, client, message, listener): 20 | print(f"Client {client.obj_id} got gmsg {message['val']}") 21 | 22 | 23 | if __name__ == "__main__": 24 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 25 | cl = cloudlink() 26 | 27 | # Create examples for various ways to extend the functionality of Cloudlink Server. 28 | example = example_events() 29 | 30 | # Example - Multiple clients. 31 | multi_client = cl.multi_client(async_client=False, logs=True) 32 | 33 | # Spawns 5 clients. 34 | for x in range(5): 35 | # Create a new client object. This supports initializing many clients at once. 36 | client = multi_client.spawn(x, "ws://127.0.0.1:3000/") 37 | 38 | # Binding events - This example binds functions to certain events 39 | 40 | # When a client connects, all functions bound to this event will fire. 41 | client.bind_event( 42 | client.events.on_connect, 43 | example.on_connect 44 | ) 45 | 46 | # When a client disconnects, all functions bound to this event will fire. 47 | client.bind_event( 48 | client.events.on_close, 49 | example.on_close 50 | ) 51 | 52 | # When a client disconnects, all functions bound to this event will fire. 53 | client.bind_event( 54 | client.events.on_username_set, 55 | example.username_set 56 | ) 57 | 58 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 59 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 60 | 61 | print("Waking up now") 62 | multi_client.run() 63 | input("All clients are ready. Press enter to shutdown.") 64 | multi_client.stop() 65 | input("All clients have shut down. Press enter to exit.") 66 | 67 | # Example - Singular clients. 68 | client = cl.client(async_client=False, logs=True) 69 | 70 | # Object IDs - Sets a friendly name to a specific client object. 71 | client.obj_id = "Test" 72 | 73 | # Binding events - This example binds functions to certain events 74 | 75 | # When a client connects, all functions bound to this event will fire. 76 | client.bind_event( 77 | client.events.on_connect, 78 | example.on_connect 79 | ) 80 | 81 | # When a client disconnects, all functions bound to this event will fire. 82 | client.bind_event( 83 | client.events.on_close, 84 | example.on_close 85 | ) 86 | 87 | # When a client disconnects, all functions bound to this event will fire. 88 | client.bind_event( 89 | client.events.on_username_set, 90 | example.username_set 91 | ) 92 | 93 | # Binding callbacks for commands - This example binds an event when a gmsg packet is handled. 94 | client.bind_callback_method(client.cl_methods.gmsg, example.on_gmsg) 95 | 96 | # Run the client. 97 | client.run("ws://127.0.0.1:3000/") 98 | -------------------------------------------------------------------------------- /golang/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= 4 | github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/fasthttp/websocket v1.5.4 h1:Bq8HIcoiffh3pmwSKB8FqaNooluStLQQxnzQspMatgI= 7 | github.com/fasthttp/websocket v1.5.4/go.mod h1:R2VXd4A6KBspb5mTrsWnZwn6ULkX56/Ktk8/0UNSJao= 8 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 9 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 10 | github.com/gofiber/contrib/websocket v1.2.2 h1:6lygrypMM0LqfPUC8N5MZ5apsU9/3K/NJULrIVpS8FU= 11 | github.com/gofiber/contrib/websocket v1.2.2/go.mod h1:QPOQ5qazfR/oz7FZD4p5PO9B8TaxjAnaUG/xpbFI1r4= 12 | github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE= 13 | github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 14 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 15 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 17 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 18 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 19 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 20 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 21 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 22 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 24 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 27 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 28 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= 29 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 30 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 31 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 32 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 33 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 34 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 35 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 36 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 37 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 40 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | -------------------------------------------------------------------------------- /golang/server/scratch.go: -------------------------------------------------------------------------------- 1 | package cloudlink 2 | 3 | import ( 4 | "github.com/gofiber/contrib/websocket" 5 | ) 6 | 7 | func ScratchProtocolDetect(client *Client) { 8 | if client.protocol == 0 { 9 | // Update client attributes 10 | client.Lock() 11 | client.protocol = 2 // Scratch 12 | client.Unlock() 13 | } 14 | } 15 | 16 | // ScratchMethodHandler is a method that gets created when a Scratch-formatted message gets handled by MessageHandler. 17 | func ScratchMethodHandler(client *Client, message *Scratch) { 18 | switch message.Method { 19 | case "handshake": 20 | 21 | // Validate datatype of project ID 22 | switch message.ProjectID.(type) { 23 | case string: 24 | case bool: 25 | case int64: 26 | case float64: 27 | default: 28 | client.CloseWithMessage(websocket.CloseUnsupportedData, "Invalid Project ID datatype") 29 | return 30 | } 31 | 32 | // Update client attributes 33 | client.username = message.Username 34 | 35 | // Creates room if it does not exist already 36 | room := client.manager.CreateRoom(message.ProjectID) 37 | 38 | // Add the client to the room 39 | room.SubscribeClient(client) 40 | 41 | // Update variable states 42 | room.gvarStateMutex.RLock() 43 | for name, value := range room.gvarState { 44 | MulticastMessage(room.clients, &Scratch{ 45 | Method: "set", 46 | Value: value, 47 | Name: name, 48 | }) 49 | } 50 | room.gvarStateMutex.RUnlock() 51 | 52 | case "set": 53 | for _, room := range client.rooms { // Should only ever have 1 entry 54 | // Update room gvar state 55 | room.gvarStateMutex.Lock() 56 | room.gvarState[message.Name] = message.Value 57 | room.gvarStateMutex.Unlock() 58 | 59 | // Broadcast the new state 60 | room.gvarStateMutex.RLock() 61 | MulticastMessage(room.clients, &Scratch{ 62 | Method: "set", 63 | Value: room.gvarState[message.Name], 64 | Name: message.Name, 65 | }) 66 | room.gvarStateMutex.RUnlock() 67 | } 68 | 69 | case "create": 70 | for _, room := range client.rooms { // Should only ever have 1 entry 71 | 72 | // Update room gvar state 73 | room.gvarStateMutex.Lock() 74 | room.gvarState[message.Name] = message.Value 75 | room.gvarStateMutex.Unlock() 76 | 77 | // Broadcast the new state 78 | room.gvarStateMutex.RLock() 79 | MulticastMessage(room.clients, &Scratch{ 80 | Method: "create", 81 | Value: room.gvarState[message.Name], 82 | Name: message.Name, 83 | }) 84 | room.gvarStateMutex.RUnlock() 85 | } 86 | 87 | case "rename": 88 | for _, room := range client.rooms { // Should only ever have 1 entry 89 | 90 | // Retrive old value 91 | room.gvarStateMutex.RLock() 92 | oldvalue := room.gvarState[message.Name] 93 | room.gvarStateMutex.RUnlock() 94 | 95 | // Destroy old value and make a new value 96 | room.gvarStateMutex.Lock() 97 | delete(room.gvarState, message.Name) 98 | room.gvarState[message.NewName] = oldvalue 99 | room.gvarStateMutex.Unlock() 100 | 101 | // Broadcast the new state 102 | MulticastMessage(room.clients, &Scratch{ 103 | Method: "rename", 104 | NewName: message.NewName, 105 | Name: message.Name, 106 | }) 107 | } 108 | 109 | case "delete": 110 | for _, room := range client.rooms { // Should only ever have 1 entry 111 | 112 | // Destroy value 113 | room.gvarStateMutex.Lock() 114 | delete(room.gvarState, message.Name) 115 | room.gvarStateMutex.Unlock() 116 | 117 | // Broadcast the new state 118 | MulticastMessage(room.clients, &Scratch{ 119 | Method: "delete", 120 | Name: message.Name, 121 | }) 122 | } 123 | 124 | default: 125 | break 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /python/archived/0.1.8.2/cloudlink/server.py: -------------------------------------------------------------------------------- 1 | from .websocket_server import WebsocketServer 2 | from .supporter import supporter 3 | from .serverRootHandlers import serverRootHandlers 4 | from .serverInternalHandlers import serverInternalHandlers 5 | 6 | class server: 7 | def __init__(self, parentCl, enable_logs): 8 | # Read the CloudLink version from the parent class 9 | self.version = parentCl.version 10 | 11 | # Init stuff for the server 12 | self.userlist = [] 13 | 14 | # Rooms system 15 | self.roomData = { 16 | "default": set() 17 | } 18 | 19 | self.motd_enable = False 20 | self.motd_msg = "" 21 | self.global_msg = "" 22 | 23 | # Init modules 24 | self.supporter = supporter(self, enable_logs) 25 | self.serverRootHandlers = serverRootHandlers(self) 26 | self.serverInternalHandlers = serverInternalHandlers(self) 27 | 28 | # Load built-in commands (automatically generates attributes for callbacks) 29 | self.builtInCommands = [] 30 | self.customCommands = [] 31 | self.disabledCommands = [] 32 | self.usercallbacks = {} 33 | self.supporter.loadBuiltinCommands() 34 | 35 | # Extra configuration 36 | self.ipblocklist = [] # Use to block IP addresses 37 | self.rejectClientMode = False # Set to true to reject future clients until false 38 | 39 | # Create API 40 | self.loadCustomCommands = self.supporter.loadCustomCommands 41 | self.disableCommands = self.supporter.disableCommands 42 | self.sendPacket = self.supporter.sendPacket 43 | self.sendCode = self.supporter.sendCode 44 | self.linkClientToRooms = self.supporter.linkClientToRooms 45 | self.unlinkClientFromRooms = self.supporter.unlinkClientFromRooms 46 | self.getAllUsersInRoom = self.supporter.getAllUsersInRoom 47 | self.getAllUsersInManyRooms = self.supporter.getAllUsersInManyRooms 48 | self.getAllRooms = self.supporter.getAllRooms 49 | self.getAllClientRooms = self.supporter.getAllClientRooms 50 | self.getUsernames = self.supporter.getUsernames 51 | self.setClientUsername = self.supporter.setClientUsername 52 | self.log = self.supporter.log 53 | 54 | # Create stuff for the callback system 55 | self.on_packet = self.serverRootHandlers.on_packet 56 | self.on_connect = self.serverRootHandlers.on_connect 57 | self.on_close = self.serverRootHandlers.on_close 58 | 59 | def run(self, port=3000, host="127.0.0.1"): 60 | # Initialize the Websocket Server 61 | self.log("Cloudlink server starting up now...") 62 | self.wss = WebsocketServer(host, port) 63 | 64 | # Bind built-in callbacks 65 | self.wss.set_fn_new_client(self.serverRootHandlers.on_connect) 66 | self.wss.set_fn_message_received(self.serverRootHandlers.on_packet) 67 | self.wss.set_fn_client_left(self.serverRootHandlers.on_close) 68 | 69 | # Create attributes 70 | self.all_clients = self.wss.clients 71 | 72 | # Run the CloudLink server 73 | self.wss.run_forever() 74 | self.log("Cloudlink server exiting...") 75 | 76 | def setMOTD(self, enable:bool, msg:str): 77 | self.motd_enable = enable 78 | self.motd_msg = msg 79 | 80 | def callback(self, callback_id, function): 81 | # Support older servers which use the old callback system. 82 | if type(callback_id) == str: 83 | callback_id = getattr(self, callback_id) 84 | 85 | # New callback system. 86 | if callable(callback_id): 87 | if callback_id == self.on_packet: 88 | self.usercallbacks[self.on_packet] = function 89 | elif callback_id == self.on_connect: 90 | self.usercallbacks[self.on_connect] = function 91 | elif callback_id == self.on_close: 92 | self.usercallbacks[self.on_close] = function 93 | elif callback_id == self.on_error: 94 | self.usercallbacks[self.on_error] = function 95 | -------------------------------------------------------------------------------- /python/archived/0.1.8.1/cloudlink/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from .websocket_server import WebsocketServer 4 | from .supporter import supporter 5 | from .serverRootHandlers import serverRootHandlers 6 | from .serverInternalHandlers import serverInternalHandlers 7 | 8 | class server: 9 | def __init__(self, parentCl, enable_logs): 10 | # Read the CloudLink version from the parent class 11 | self.version = parentCl.version 12 | 13 | # Init stuff for the server 14 | self.userlist = [] 15 | 16 | # Rooms system 17 | self.roomData = { 18 | "default": [] 19 | } 20 | 21 | self.motd_enable = False 22 | self.motd_msg = "" 23 | self.global_msg = "" 24 | 25 | # Init modules 26 | self.supporter = supporter(self, enable_logs) 27 | self.log = self.supporter.log 28 | self.serverRootHandlers = serverRootHandlers(self) 29 | self.serverInternalHandlers = serverInternalHandlers(self) 30 | 31 | # Load built-in commands (automatically generates attributes for callbacks) 32 | self.builtInCommands = [] 33 | self.customCommands = [] 34 | self.disabledCommands = [] 35 | self.usercallbacks = {} 36 | self.supporter.loadBuiltinCommands() 37 | 38 | # Extra configuration 39 | self.ipblocklist = [] # Use to block IP addresses 40 | self.rejectClientMode = False # Set to true to reject future clients until false 41 | 42 | # Create API 43 | self.loadCustomCommands = self.supporter.loadCustomCommands 44 | self.disableCommands = self.supporter.disableCommands 45 | self.sendPacket = self.supporter.sendPacket 46 | self.sendCode = self.supporter.sendCode 47 | self.linkClientToRooms = self.supporter.linkClientToRooms 48 | self.unlinkClientFromRooms = self.supporter.unlinkClientFromRooms 49 | self.getAllUsersInRoom = self.supporter.getAllUsersInRoom 50 | self.getAllUsersInManyRooms = self.supporter.getAllUsersInManyRooms 51 | self.getAllRooms = self.supporter.getAllRooms 52 | self.getAllClientRooms = self.supporter.getAllClientRooms 53 | self.getUsernames = self.supporter.getUsernames 54 | self.setClientUsername = self.supporter.setClientUsername 55 | 56 | # Create stuff for the callback system 57 | self.on_packet = self.serverRootHandlers.on_packet 58 | self.on_connect = self.serverRootHandlers.on_connect 59 | self.on_close = self.serverRootHandlers.on_close 60 | 61 | def run(self, port=3000, host="127.0.0.1"): 62 | # Initialize the Websocket Server 63 | self.log("Cloudlink server starting up now...") 64 | self.wss = WebsocketServer(host, port) 65 | 66 | # Bind built-in callbacks 67 | self.wss.set_fn_new_client(self.serverRootHandlers.on_connect) 68 | self.wss.set_fn_message_received(self.serverRootHandlers.on_packet) 69 | self.wss.set_fn_client_left(self.serverRootHandlers.on_close) 70 | 71 | # Create attributes 72 | self.all_clients = self.wss.clients 73 | 74 | # Run the CloudLink server 75 | self.wss.run_forever() 76 | self.log("Cloudlink server exiting...") 77 | 78 | def setMOTD(self, enable:bool, msg:str): 79 | self.motd_enable = enable 80 | self.motd_msg = msg 81 | 82 | def callback(self, callback_id, function): 83 | # Support older servers which use the old callback system. 84 | if type(callback_id) == str: 85 | callback_id = getattr(self, callback_id) 86 | 87 | # New callback system. 88 | if callable(callback_id): 89 | if callback_id == self.on_packet: 90 | self.usercallbacks[self.on_packet] = function 91 | elif callback_id == self.on_connect: 92 | self.usercallbacks[self.on_connect] = function 93 | elif callback_id == self.on_close: 94 | self.usercallbacks[self.on_close] = function 95 | elif callback_id == self.on_error: 96 | self.usercallbacks[self.on_error] = function 97 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/server-example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import Cloudlink 2 | 3 | class customCommands: 4 | """ 5 | customCommands 6 | 7 | This is an example of Cloudlink 4.0's custom commands system. 8 | """ 9 | 10 | def __init__(self, cloudlink, extra_data:any = None): 11 | # To use custom commands, you will need to initialize your custom commands class with Cloudlink. This is required. 12 | self.cloudlink = cloudlink 13 | 14 | # You can specify which functions to ignore when using Cloudlink.server.loadCustomCommands. This is optional. 15 | self.importer_ignore_functions = [] # ["test"] if you don't want to load the custom command "test". 16 | 17 | # You can specify extra data to this class, see __main__ below. 18 | self.extra_data = extra_data 19 | 20 | # Optionally, you can reference Cloudlink components for extended functionality. 21 | self.supporter = self.cloudlink.supporter 22 | 23 | def test(self, client, server, message, listener_detected, listener_id, room_id): 24 | """ 25 | To define a custom command, a command must contain the following parameters: 26 | self, client, server, message, listener_detected, listener_id, room_id 27 | 28 | self - Should be a class reference to itself. 29 | client - Dictionary object that identifies clients. Contains headers, address, handler, and id. See /cloudlink/websocket_server/websocket_server.py for info. 30 | server - Required for the websocket server and for backward compatibility. 31 | message - The command's payload. 32 | listener_detected - Boolean that is set when Cloudlink.server.serverRootHandlers checks a packet and identifies the JSON key "listener" in a packet. 33 | listener_id - Any value that is set when Cloudlink.server.serverRootHandlers checks a packet and reads the JSON key "listener" in a packet. 34 | room_id - Array of strings that is set when a client has been linked to rooms. Defaults to either None or ["default"]. 35 | 36 | Most of the time, you will be passing listener_detected, listener_id, and room_id to various cloudlink functions. You will likely never use server. 37 | """ 38 | self.cloudlink.sendPacket(client, {"cmd": "direct", "val": "test"}, listener_detected, listener_id, room_id) 39 | 40 | class demoCallbacksServer: 41 | """ 42 | demoCallbacksServer 43 | 44 | This is an example of Cloudlink's callback system. 45 | """ 46 | 47 | def __init__(self, cloudlink): 48 | # To use callbacks, you will need to initialize your callbacks class with Cloudlink. This is required. 49 | self.cloudlink = cloudlink 50 | 51 | def on_packet(self, client, server, message): 52 | print("on_packet fired!") 53 | 54 | def on_connect(self, client, server): 55 | print("on_connect fired!") 56 | 57 | def on_close(self, client, server): 58 | print("on_close fired!") 59 | 60 | if __name__ == "__main__": 61 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 62 | cl = Cloudlink() 63 | 64 | # Create a new server object. This supports initializing many server at once. 65 | server = cl.server(logs=True) 66 | 67 | # Set the server's Message-Of-The-Day. 68 | server.setMOTD(True, "CloudLink 4 Test") 69 | 70 | # Create demo callbacks. You can only initialize callbacks after you have initialized a cloudlink server object. 71 | dummy = demoCallbacksServer(server) 72 | 73 | # Bind demo callbacks 74 | server.callback(server.on_packet, dummy.on_packet) 75 | server.callback(server.on_connect, dummy.on_connect) 76 | server.callback(server.on_close, dummy.on_close) 77 | 78 | # To pass custom commands, simply pass a list containing uninitialized classes. 79 | # To specify custom parameters, pass a dictionary object with an uninitialized class as a key and your custom parameters as it's value. 80 | #client.loadCustomCommands(customCommands, {customCommands: dummy}) 81 | server.loadCustomCommands(customCommands) 82 | 83 | # Command disabler. Simply pass a list of strings containing either CLPv4 commands to ignore, or custom commands to unload. 84 | #server.disableCommands(["gmsg"]) 85 | 86 | server.run(host="0.0.0.0", port=3000) -------------------------------------------------------------------------------- /python/archived/0.1.8.0/cloudlink/cloudlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | CloudLink Server 5 | 6 | CloudLink is a free and open-source, websocket-powered API optimized for Scratch 3.0. 7 | For documentation, please visit https://hackmd.io/g6BogABhT6ux1GA2oqaOXA 8 | 9 | Server based on https://github.com/Pithikos/python-websocket-server 10 | The WebsocketServer that is bundled with CloudLink is modified to support Cloudflared and fixes an issue with asserting the HTTP websocket upgrade request. 11 | 12 | Please see https://github.com/MikeDev101/cloudlink for more details. 13 | """ 14 | 15 | class Cloudlink: 16 | def __init__(self): 17 | self.version = "0.1.8.0" 18 | 19 | def server(self, logs=False): 20 | # Initialize Cloudlink server 21 | from server import server 22 | return server(self, logs) 23 | 24 | def client(self, server_ip = "ws://127.0.0.1:3000/", logs=False): 25 | # TODO 26 | pass 27 | 28 | def relay(self, server_ip = "ws://127.0.0.1:3000/", relay_ip = "127.0.0.1", relay_port = 3000, logs=False): 29 | # TODO 30 | pass 31 | 32 | class customCommands: 33 | """ 34 | customCommands 35 | 36 | This is an example of Cloudlink 4.0's new custom commands system. 37 | """ 38 | 39 | def __init__(self, cloudlink): 40 | # To use custom commands, you will need to initialize your custom commands class with Cloudlink. This is required. 41 | self.cloudlink = cloudlink 42 | # You can specify which functions to ignore when using Cloudlink.server.loadCustomCommands. This is optional. 43 | self.importer_ignore_functions = [] # ["test"] if you don't want to load the custom command "test". 44 | 45 | # Optionally, you can reference Cloudlink components for extended functionality. 46 | self.supporter = self.cloudlink.supporter 47 | 48 | def test(self, client, server, message, listener_detected, listener_id, room_id): 49 | """ 50 | To define a custom command, a command must contain the following parameters: 51 | self, client, server, message, listener_detected, listener_id, room_id 52 | 53 | self - Should be a class reference to itself. 54 | client - Dictionary object that identifies clients. Contains headers, address, handler, and id. See /cloudlink/websocket_server/websocket_server.py for info. 55 | server - Required for the websocket server and for backward compatibility. 56 | listener_detected - Boolean that is set when Cloudlink.server.serverRootHandlers checks a packet and identifies the JSON key "listener" in a packet. 57 | listener_id - Any value that is set when Cloudlink.server.serverRootHandlers checks a packet and reads the JSON key "listener" in a packet. 58 | room_id - Array of strings that is set when a client has been linked to rooms. Defaults to either None or ["default"]. 59 | 60 | Most of the time, you will be passing listener_detected, listener_id, and room_id to various cloudlink functions. You will likely never use server. 61 | """ 62 | self.cloudlink.sendPacket(client, {"cmd": "direct", "val": "test"}, listener_detected, listener_id, room_id) 63 | 64 | class demoCallbacks: 65 | """ 66 | demoCallbacks 67 | 68 | This is an example of Cloudlink's callback compatibility system, which re-implements callbacks used in older versions of Cloudlink. 69 | It is not recommended to use this feature in newer implementations as it has been replaced with Cloudlink.server.loadCustomCommands. 70 | """ 71 | 72 | def __init__(self): 73 | pass 74 | 75 | def on_packet(self, client, server, message): 76 | #print("on_packet") 77 | pass 78 | 79 | def on_connect(self, client, server): 80 | #print("on_connect") 81 | pass 82 | 83 | def on_close(self, client, server): 84 | #print("on_close") 85 | pass 86 | 87 | if __name__ == "__main__": 88 | cl = Cloudlink() 89 | dummy = demoCallbacks() 90 | 91 | server = cl.server(logs=True) 92 | server.setMOTD(True, "CloudLink 4 Test") 93 | 94 | server.callback(server.on_packet, dummy.on_packet) 95 | server.callback(server.on_connect, dummy.on_connect) 96 | server.callback(server.on_close, dummy.on_close) 97 | 98 | server.loadCustomCommands(customCommands) 99 | #server.disableCommands(["gmsg"]) 100 | 101 | server.run(host="0.0.0.0", port=3000) -------------------------------------------------------------------------------- /python/archived/0.1.9.1/server_example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import cloudlink 2 | 3 | 4 | class example_callbacks: 5 | def __init__(self, parent): 6 | self.parent = parent 7 | 8 | async def test1(self, client, message, listener): 9 | print("Test1!") 10 | await self.parent.asyncio.sleep(1) 11 | print("Test1 after one second!") 12 | 13 | async def test2(self, client, message, listener): 14 | print("Test2!") 15 | await self.parent.asyncio.sleep(1) 16 | print("Test2 after one second!") 17 | 18 | async def test3(self, client, message, listener): 19 | print("Test3!") 20 | 21 | 22 | class example_events: 23 | def __init__(self): 24 | pass 25 | 26 | async def on_close(self, client): 27 | print("Client", client.id, "disconnected.") 28 | 29 | async def on_connect(self, client): 30 | print("Client", client.id, "connected.") 31 | 32 | 33 | class example_commands: 34 | def __init__(self, parent): 35 | self.parent = parent 36 | self.supporter = parent.supporter 37 | 38 | # If you want to have commands with very specific formatting, use the validate() function. 39 | self.validate = parent.validate 40 | 41 | # Various ways to send messages 42 | self.send_packet_unicast = parent.send_packet_unicast 43 | self.send_packet_multicast = parent.send_packet_multicast 44 | self.send_packet_multicast_variable = parent.send_packet_multicast_variable 45 | self.send_code = parent.send_code 46 | 47 | async def foobar(self, client, message, listener): 48 | print("Foobar!") 49 | 50 | # Reading the IP address of the client is as easy as calling get_client_ip from the server object. 51 | print(self.parent.get_client_ip(client)) 52 | 53 | # In case you need to report a status code, use send_code. 54 | await self.send_code( 55 | client=client, 56 | code="OK", 57 | listener=listener 58 | ) 59 | 60 | 61 | if __name__ == "__main__": 62 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 63 | cl = cloudlink() 64 | 65 | # Create a new server object. This supports initializing many servers at once. 66 | server = cl.server(logs=True) 67 | 68 | # Create examples for various ways to extend the functionality of Cloudlink Server. 69 | callbacks = example_callbacks(server) 70 | commands = example_commands(server) 71 | events = example_events() 72 | 73 | # Set the message-of-the-day. 74 | server.set_motd("CL4 Optimized! Gotta Go Fast!", True) 75 | 76 | # Here are some extra parameters you can specify to change the functionality of the server. 77 | 78 | # Defaults to empty list. Requires having check_ip_addresses set to True. 79 | # server.ip_blocklist = ["127.0.0.1"] 80 | 81 | # Defaults to False. If True, the server will refuse all connections until False. 82 | # server.reject_clients = False 83 | 84 | # Defaults to False. If True, client IP addresses will be resolved and stored until a client disconnects. 85 | # server.check_ip_addresses = True 86 | 87 | # Defaults to True. If True, the server will support Scratch's cloud variable protocol. 88 | # server.enable_scratch_support = False 89 | 90 | # Binding callbacks - This example binds the "handshake" command with example callbacks. 91 | # You can bind as many functions as you want to a callback, but they must use async. 92 | # To bind callbacks to built-in methods (example: gmsg), see cloudlink.cl_methods. 93 | server.bind_callback(server.cl_methods.handshake, callbacks.test1) 94 | server.bind_callback(server.cl_methods.handshake, callbacks.test2) 95 | 96 | # Binding events - This example will print a client connect/disconnect message. 97 | # You can bind as many functions as you want to an event, but they must use async. 98 | # To see all possible events for the server, see cloudlink.events. 99 | server.bind_event(server.events.on_connect, events.on_connect) 100 | server.bind_event(server.events.on_close, events.on_close) 101 | 102 | # Creating custom commands - This example adds a custom command "foobar" from example_commands 103 | # and then binds the callback test3 to the new command. 104 | server.load_custom_methods(commands) 105 | server.bind_callback(commands.foobar, callbacks.test3) 106 | 107 | # Run the server. 108 | server.run(ip="localhost", port=3000) 109 | -------------------------------------------------------------------------------- /python/archived/0.1.9.2/server_example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import cloudlink 2 | 3 | 4 | class example_callbacks: 5 | def __init__(self, parent): 6 | self.parent = parent 7 | 8 | async def test1(self, client, message, listener): 9 | print("Test1!") 10 | await self.parent.asyncio.sleep(1) 11 | print("Test1 after one second!") 12 | 13 | async def test2(self, client, message, listener): 14 | print("Test2!") 15 | await self.parent.asyncio.sleep(1) 16 | print("Test2 after one second!") 17 | 18 | async def test3(self, client, message, listener): 19 | print("Test3!") 20 | 21 | 22 | class example_events: 23 | def __init__(self): 24 | pass 25 | 26 | async def on_close(self, client): 27 | print("Client", client.id, "disconnected.") 28 | 29 | async def on_connect(self, client): 30 | print("Client", client.id, "connected.") 31 | 32 | 33 | class example_commands: 34 | def __init__(self, parent): 35 | self.parent = parent 36 | self.supporter = parent.supporter 37 | 38 | # If you want to have commands with very specific formatting, use the validate() function. 39 | self.validate = parent.validate 40 | 41 | # Various ways to send messages 42 | self.send_packet_unicast = parent.send_packet_unicast 43 | self.send_packet_multicast = parent.send_packet_multicast 44 | self.send_packet_multicast_variable = parent.send_packet_multicast_variable 45 | self.send_code = parent.send_code 46 | 47 | async def foobar(self, client, message, listener): 48 | print("Foobar!") 49 | 50 | # Reading the IP address of the client is as easy as calling get_client_ip from the server object. 51 | print(self.parent.get_client_ip(client)) 52 | 53 | # In case you need to report a status code, use send_code. 54 | await self.send_code( 55 | client=client, 56 | code="OK", 57 | listener=listener 58 | ) 59 | 60 | 61 | if __name__ == "__main__": 62 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 63 | cl = cloudlink() 64 | 65 | # Create a new server object. This supports initializing many servers at once. 66 | server = cl.server(logs=True) 67 | 68 | # Create examples for various ways to extend the functionality of Cloudlink Server. 69 | callbacks = example_callbacks(server) 70 | commands = example_commands(server) 71 | events = example_events() 72 | 73 | # Set the message-of-the-day. 74 | server.set_motd("CL4 Optimized! Gotta Go Fast!", True) 75 | 76 | # Here are some extra parameters you can specify to change the functionality of the server. 77 | 78 | # Defaults to empty list. Requires having check_ip_addresses set to True. 79 | # server.ip_blocklist = ["127.0.0.1"] 80 | 81 | # Defaults to False. If True, the server will refuse all connections until False. 82 | # server.reject_clients = False 83 | 84 | # Defaults to False. If True, client IP addresses will be resolved and stored until a client disconnects. 85 | # server.check_ip_addresses = True 86 | 87 | # Defaults to True. If True, the server will support Scratch's cloud variable protocol. 88 | # server.enable_scratch_support = False 89 | 90 | # Binding callbacks - This example binds the "handshake" command with example callbacks. 91 | # You can bind as many functions as you want to a callback, but they must use async. 92 | # To bind callbacks to built-in methods (example: gmsg), see cloudlink.cl_methods. 93 | server.bind_callback(server.cl_methods.handshake, callbacks.test1) 94 | server.bind_callback(server.cl_methods.handshake, callbacks.test2) 95 | 96 | # Binding events - This example will print a client connect/disconnect message. 97 | # You can bind as many functions as you want to an event, but they must use async. 98 | # To see all possible events for the server, see cloudlink.events. 99 | server.bind_event(server.events.on_connect, events.on_connect) 100 | server.bind_event(server.events.on_close, events.on_close) 101 | 102 | # Creating custom commands - This example adds a custom command "foobar" from example_commands 103 | # and then binds the callback test3 to the new command. 104 | server.load_custom_methods(commands) 105 | server.bind_callback(commands.foobar, callbacks.test3) 106 | 107 | # Run the server. 108 | server.run(ip="localhost", port=3000) 109 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/client-example.py: -------------------------------------------------------------------------------- 1 | from cloudlink import Cloudlink 2 | 3 | class demoCallbacksClient: 4 | """ 5 | demoCallbacksClient 6 | 7 | This is an example of Cloudlink's callback system. 8 | """ 9 | 10 | def __init__(self, cloudlink): 11 | # To use callbacks, you will need to initialize your callbacks class with Cloudlink. This is required. 12 | self.cloudlink = cloudlink 13 | 14 | # Below are templates for binding generic callbacks. 15 | 16 | def on_packet(self, message): # Called when any packet is received, regardless of packet command. 17 | print("on_packet fired!") 18 | 19 | def on_connect(self): # Called when the client is connected to the server. 20 | print("on_connect fired!") 21 | self.cloudlink.sendGlobalMessage("this is a test") 22 | self.cloudlink.setUsername("test") 23 | 24 | def on_close(self, close_status_code, close_msg): # Called when the client is disconnected from the server. 25 | print("on_close fired!") 26 | 27 | def on_error(self, error): # Called when the client encounters an exception. 28 | print("on_error fired!") 29 | 30 | # Below are templates for binding command-specific callbacks. 31 | 32 | def on_direct(self, message:any): # Called when a packet is received with the direct command. 33 | print("on_direct fired!") 34 | #pass 35 | 36 | def on_version(self, version:str): # Called when a packet is received with the server_version command. 37 | print("on_version fired!") 38 | # pass 39 | 40 | def on_motd(self, motd:str): # Called when a packet is received with the motd command. 41 | print("on_motd fired!") 42 | # pass 43 | 44 | def on_ip(self, ip:str): # Called when a packet is received with the client_ip command. 45 | print("on_ip fired!") 46 | # pass 47 | 48 | def on_ulist(self, ulist:list): # Called when a packet is received with the ulist command. 49 | print("on_ulist fired!") 50 | # pass 51 | 52 | def on_statuscode(self, code:str): # Called when a packet is received with the statuscode command. 53 | print("on_statuscode fired!") 54 | # pass 55 | 56 | def on_gmsg(self, message:str): # Called when a packet is received with the gmsg command. 57 | print("on_gmsg fired!") 58 | # pass 59 | 60 | def on_gvar(self, var_name:str, var_value:any): # Called when a packet is received with the gvar command. 61 | print("on_gvar fired!") 62 | # pass 63 | 64 | def on_pvar(self, var_name:str, var_value:any, origin:any): # Called when a packet is received with the pvar command. 65 | print("on_pvar fired!") 66 | # pass 67 | 68 | def on_pmsg(self, value:str, origin:any): # Called when a packet is received with the pmsg command. 69 | print("on_pmsg fired!") 70 | # pass 71 | 72 | def on_ping(self, value:str, origin:any): # Called when the client is being pinged by another client (It will automatically respond to the ping, this is just used for diagnostics). 73 | print("on_ping fired!") 74 | # pass 75 | 76 | if __name__ == "__main__": 77 | # Initialize Cloudlink. You will only need to initialize one instance of the main cloudlink module. 78 | cl = Cloudlink() 79 | 80 | # Create a new client object. This supports initializing many clients at once. 81 | client = cl.client(logs=True) 82 | 83 | # Create demo callbacks. You can only initialize callbacks after you have initialized a cloudlink client object. 84 | dummy = demoCallbacksClient(client) 85 | 86 | # Bind demo callbacks 87 | client.callback(client.on_packet, dummy.on_packet) 88 | client.callback(client.on_connect, dummy.on_connect) 89 | client.callback(client.on_close, dummy.on_close) 90 | client.callback(client.on_error, dummy.on_error) 91 | 92 | # Bind template callbacks 93 | client.callback(client.on_direct, dummy.on_direct) 94 | client.callback(client.on_version, dummy.on_version) 95 | client.callback(client.on_motd, dummy.on_motd) 96 | client.callback(client.on_ip, dummy.on_ip) 97 | client.callback(client.on_ulist, dummy.on_ulist) 98 | client.callback(client.on_statuscode, dummy.on_statuscode) 99 | client.callback(client.on_gmsg, dummy.on_gmsg) 100 | client.callback(client.on_gvar, dummy.on_gvar) 101 | client.callback(client.on_pvar, dummy.on_pvar) 102 | client.callback(client.on_pmsg, dummy.on_pmsg) 103 | client.callback(client.on_ping, dummy.on_ping) 104 | 105 | # Command disabler. Simply pass a list of strings containing CLPv4 commands to ignore. 106 | #client.disableCommands(["gmsg"]) 107 | 108 | # Connect to the server and run the client. 109 | client.run(ip="ws://127.0.0.1:3000/") -------------------------------------------------------------------------------- /python/cloudlink/server/modules/clients_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | clients_manager - Provides tools to search, add, and remove clients from the server. 3 | """ 4 | 5 | 6 | class exceptions: 7 | class ClientDoesNotExist(Exception): 8 | """This exception is raised when a client object does not exist""" 9 | pass 10 | 11 | class ClientAlreadyExists(Exception): 12 | """This exception is raised when attempting to add a client object that is already present""" 13 | pass 14 | 15 | class ClientUsernameAlreadySet(Exception): 16 | """This exception is raised when a client attempts to set their friendly username, but it was already set.""" 17 | pass 18 | 19 | class ClientUsernameNotSet(Exception): 20 | """This exception is raised when a client object has not yet set it's friendly username.""" 21 | pass 22 | 23 | class NoResultsFound(Exception): 24 | """This exception is raised when there are no results for a client search request.""" 25 | pass 26 | 27 | class ProtocolAlreadySet(Exception): 28 | """This exception is raised when attempting to change a client's protocol.""" 29 | pass 30 | 31 | 32 | class clients_manager: 33 | def __init__(self, parent): 34 | # Inherit parent 35 | self.parent = parent 36 | 37 | # Create attributes for storage/searching 38 | self.clients = set() 39 | self.snowflakes = dict() 40 | self.protocols = dict() 41 | self.usernames = dict() 42 | self.uuids = dict() 43 | 44 | # Init exceptions 45 | self.exceptions = exceptions() 46 | 47 | # Init logger 48 | self.logging = parent.logging 49 | self.logger = self.logging.getLogger(__name__) 50 | 51 | def __len__(self): 52 | return len(self.clients) 53 | 54 | def get_snowflakes(self): 55 | return set(obj.snowflake for obj in self.clients) 56 | 57 | def get_uuids(self): 58 | return set(str(obj.id) for obj in self.clients) 59 | 60 | def exists(self, obj): 61 | return obj in self.clients 62 | 63 | def add(self, obj): 64 | if self.exists(obj): 65 | raise self.exceptions.ClientAlreadyExists 66 | 67 | # Add object to set 68 | self.clients.add(obj) 69 | 70 | # Add to snowflakes 71 | self.snowflakes[obj.snowflake] = obj 72 | 73 | # Add to UUIDs 74 | self.uuids[str(obj.id)] = obj 75 | 76 | def remove(self, obj): 77 | if not self.exists(obj): 78 | raise self.exceptions.ClientDoesNotExist 79 | 80 | # Remove from all clients set 81 | self.clients.remove(obj) 82 | 83 | # Remove from snowflakes 84 | self.snowflakes.pop(obj.snowflake) 85 | 86 | # Remove from UUIDs 87 | self.uuids.pop(str(obj.id)) 88 | 89 | # Remove from protocol references 90 | if obj.protocol_set: 91 | 92 | # Remove reference to protocol object 93 | if obj in self.protocols[obj.protocol]: 94 | self.protocols[obj.protocol].remove(obj) 95 | 96 | # Clean up unused protocol references 97 | if not len(self.protocols[obj.protocol]): 98 | del self.protocols[obj.protocol] 99 | 100 | # Remove client from username references 101 | if obj.username_set: 102 | 103 | # Remove reference to username object 104 | if obj.username in self.usernames: 105 | self.usernames[obj.username].remove(obj) 106 | 107 | # Clean up unused usernames 108 | if not len(self.usernames[obj.username]): 109 | del self.usernames[obj.username] 110 | 111 | def set_username(self, obj, username): 112 | if not self.exists(obj): 113 | raise self.exceptions.ClientDoesNotExist 114 | 115 | if obj.username_set: 116 | raise self.exceptions.ClientUsernameAlreadySet 117 | 118 | # Create username reference 119 | if username not in self.usernames: 120 | self.usernames[username] = set() 121 | 122 | # Create reference to object from its username 123 | self.usernames[username].add(obj) 124 | 125 | # Finally set attributes 126 | obj.username_set = True 127 | obj.username = username 128 | 129 | def set_protocol(self, obj, schema): 130 | if not self.exists(obj): 131 | raise self.exceptions.ClientDoesNotExist 132 | 133 | if obj.protocol_set: 134 | raise self.exceptions.ProtocolAlreadySet 135 | 136 | # If the protocol was not specified beforehand, create it 137 | if schema not in self.protocols: 138 | self.protocols[schema] = set() 139 | 140 | # Add client to the protocols identifier 141 | self.protocols[schema].add(obj) 142 | 143 | # Set client protocol 144 | obj.protocol = schema 145 | obj.protocol_set = True 146 | 147 | def find_obj(self, query): 148 | if type(query) not in [str, dict]: 149 | raise TypeError("Clients can only be usernames (str), snowflakes (str), UUIDs (str), or Objects (dict).") 150 | 151 | if query in self.usernames: 152 | return self.usernames[query] 153 | elif query in self.get_uuids(): 154 | return self.uuids[query] 155 | elif query in self.get_snowflakes(): 156 | return self.snowflakes[query] 157 | else: 158 | raise self.exceptions.NoResultsFound 159 | -------------------------------------------------------------------------------- /golang/server/manager.go: -------------------------------------------------------------------------------- 1 | package cloudlink 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | "github.com/bwmarrin/snowflake" 8 | "github.com/gofiber/contrib/websocket" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | var ServerVersion string = "0.1.0-golang" 13 | 14 | type Room struct { 15 | // Subscribed clients to the room 16 | clients map[snowflake.ID]*Client 17 | clientsMutex sync.RWMutex 18 | 19 | // Global message (GMSG) state 20 | gmsgState interface{} 21 | gmsgStateMutex sync.RWMutex 22 | 23 | // Globar variables (GVAR) states 24 | gvarState map[interface{}]any 25 | gvarStateMutex sync.RWMutex 26 | 27 | // Friendly name for room 28 | name interface{} 29 | 30 | // Locks states before subscribing/unsubscribing clients 31 | sync.RWMutex 32 | } 33 | 34 | type Manager struct { 35 | // Friendly name for manager 36 | name interface{} 37 | 38 | // Registered client sessions 39 | clients map[snowflake.ID]*Client 40 | clientsMutex sync.RWMutex 41 | 42 | // Rooms storage 43 | rooms map[any]*Room 44 | roomsMutex sync.RWMutex 45 | 46 | // Configuration settings 47 | Config struct { 48 | RejectClients bool 49 | CheckIPAddresses bool 50 | EnableMOTD bool 51 | MOTDMessage string 52 | } 53 | 54 | // Used for generating Snowflake IDs 55 | SnowflakeIDNode *snowflake.Node 56 | 57 | // Locks states before registering sessions 58 | sync.RWMutex 59 | } 60 | 61 | // NewClient assigns a UUID and Snowflake ID to a websocket client, and returns a initialized Client struct for use with a manager's AddClient. 62 | func NewClient(conn *websocket.Conn, manager *Manager) *Client { 63 | // Request and create a lock before generating ID values 64 | manager.clientsMutex.Lock() 65 | 66 | // Generate client ID values 67 | client_id := manager.SnowflakeIDNode.Generate() 68 | client_uuid := uuid.New() 69 | 70 | // Release the lock 71 | manager.clientsMutex.Unlock() 72 | 73 | return &Client{ 74 | connection: conn, 75 | manager: manager, 76 | id: client_id, 77 | uuid: client_uuid, 78 | rooms: make(map[any]*Room), 79 | handshake: false, 80 | } 81 | } 82 | 83 | // Dummy Managers function identically to a normal manager. However, they are used for selecting specific clients to multicast to. 84 | func DummyManager(name interface{}) *Manager { 85 | return &Manager{ 86 | clients: make(map[snowflake.ID]*Client), 87 | rooms: make(map[any]*Room), 88 | name: name, 89 | } 90 | } 91 | 92 | func New(name string) *Manager { 93 | node, err := snowflake.NewNode(1) 94 | if err != nil { 95 | log.Fatalln(err, 3) 96 | } 97 | 98 | manager := &Manager{ 99 | name: name, 100 | clients: make(map[snowflake.ID]*Client), 101 | rooms: make(map[any]*Room), 102 | SnowflakeIDNode: node, 103 | } 104 | 105 | return manager 106 | } 107 | 108 | func (manager *Manager) CreateRoom(name interface{}) *Room { 109 | manager.roomsMutex.RLock() 110 | 111 | // Access rooms map 112 | _, exists := manager.rooms[name] 113 | 114 | manager.roomsMutex.RUnlock() 115 | 116 | if !exists { 117 | manager.roomsMutex.Lock() 118 | 119 | log.Printf("[%s] Creating room %s", manager.name, name) 120 | 121 | // Create and prepare the room state 122 | manager.rooms[name] = &Room{ 123 | name: name, 124 | clients: make(map[snowflake.ID]*Client, 1), 125 | gmsgState: "", 126 | gvarState: make(map[any]any), 127 | } 128 | 129 | manager.roomsMutex.Unlock() 130 | } 131 | 132 | // Return the room even if it already exists 133 | return manager.rooms[name] 134 | } 135 | 136 | func (room *Room) SubscribeClient(client *Client) { 137 | room.clientsMutex.Lock() 138 | 139 | // Add client 140 | room.clients[client.id] = client 141 | 142 | room.clientsMutex.Unlock() 143 | client.Lock() 144 | 145 | // Add pointer to subscribed room in client's state 146 | client.rooms[room.name] = room 147 | 148 | client.Unlock() 149 | 150 | // Handle CL room states 151 | client.RLock() 152 | protocol := client.protocol 153 | usernameset := (client.username != nil) 154 | client.RUnlock() 155 | if protocol == 1 && usernameset { 156 | room.BroadcastUserlistEvent("add", client, false) 157 | } 158 | } 159 | 160 | func (room *Room) UnsubscribeClient(client *Client) { 161 | room.clientsMutex.Lock() 162 | 163 | // Remove client 164 | delete(room.clients, client.id) 165 | 166 | room.clientsMutex.Unlock() 167 | client.Lock() 168 | 169 | // Remove pointer to subscribed room from client's state 170 | delete(client.rooms, room.name) 171 | 172 | client.Unlock() 173 | 174 | // Handle CL room states 175 | client.RLock() 176 | protocol := client.protocol 177 | usernameset := (client.username != nil) 178 | client.RUnlock() 179 | if protocol == 1 && usernameset { 180 | room.BroadcastUserlistEvent("remove", client, true) 181 | } 182 | } 183 | 184 | func (manager *Manager) DeleteRoom(name interface{}) { 185 | manager.roomsMutex.Lock() 186 | 187 | log.Printf("[%s] Destroying room %s", manager.name, name) 188 | 189 | // Delete room 190 | delete(manager.rooms, name) 191 | 192 | manager.roomsMutex.Unlock() 193 | } 194 | 195 | func (manager *Manager) AddClient(client *Client) { 196 | manager.clientsMutex.Lock() 197 | 198 | // Add client 199 | manager.clients[client.id] = client 200 | 201 | manager.clientsMutex.Unlock() 202 | } 203 | 204 | func (manager *Manager) RemoveClient(client *Client) { 205 | manager.clientsMutex.Lock() 206 | 207 | // Remove client from manager's clients map 208 | delete(manager.clients, client.id) 209 | 210 | // Unsubscribe from all rooms and free memory by clearing out empty rooms 211 | for _, room := range TempCopyRooms(client.rooms) { 212 | room.UnsubscribeClient(client) 213 | 214 | // Destroy room if empty, but don't destroy default room 215 | if len(room.clients) == 0 && (room.name != "default") { 216 | manager.DeleteRoom(room.name) 217 | } 218 | } 219 | manager.clientsMutex.Unlock() 220 | } 221 | -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/client/clientInternalHandlers.py: -------------------------------------------------------------------------------- 1 | class clientInternalHandlers(): 2 | """ 3 | The clientInternalHandlers inter class serves as the clients's built-in command handler. 4 | These commands are hard-coded per-spec as outlined in the CLPv4 (Cloudlink Protocol) guideline. 5 | """ 6 | 7 | def __init__(self, cloudlink): 8 | self.cloudlink = cloudlink 9 | self.supporter = self.cloudlink.supporter 10 | self.importer_ignore_functions = ["relay"] 11 | 12 | def direct(self, message): 13 | self.supporter.log(f"Client received direct data: \"{message['val']}\"") 14 | 15 | # Fire callbacks 16 | if self.direct in self.cloudlink.usercallbacks: 17 | if self.cloudlink.usercallbacks[self.direct] != None: 18 | self.cloudlink.usercallbacks[self.direct](message) 19 | 20 | def server_version(self, message): 21 | self.supporter.log(f"Server reports version: {message['val']}") 22 | self.cloudlink.sever_version = message['val'] 23 | 24 | # Fire callbacks 25 | if self.server_version in self.cloudlink.usercallbacks: 26 | if self.cloudlink.usercallbacks[self.server_version] != None: 27 | self.cloudlink.usercallbacks[self.server_version](message['val']) 28 | 29 | def motd(self, message): 30 | self.supporter.log(f"Message of the day: \"{message['val']}\"") 31 | self.cloudlink.motd_msg = message['val'] 32 | 33 | # Fire callbacks 34 | if self.motd in self.cloudlink.usercallbacks: 35 | if self.cloudlink.usercallbacks[self.motd] != None: 36 | self.cloudlink.usercallbacks[self.motd](message['val']) 37 | 38 | def client_ip(self, message): 39 | self.supporter.log(f"Server reports client IP: {message['val']}") 40 | self.cloudlink.ip_address = message['val'] 41 | 42 | # Fire callbacks 43 | if self.client_ip in self.cloudlink.usercallbacks: 44 | if self.cloudlink.usercallbacks[self.client_ip] != None: 45 | self.cloudlink.usercallbacks[self.client_ip](message["val"]) 46 | 47 | def ulist(self, message): 48 | self.supporter.log(f"Userlist updated: {message['val']}") 49 | 50 | if "room" in message: 51 | self.cloudlink.userlist[message['room']] = message['val'] 52 | else: 53 | self.cloudlink.userlist["default"] = message['val'] 54 | 55 | # Fire callbacks 56 | if self.ulist in self.cloudlink.usercallbacks: 57 | if self.cloudlink.usercallbacks[self.ulist] != None: 58 | self.cloudlink.usercallbacks[self.ulist](self.cloudlink.userlist) 59 | 60 | # Status codes 61 | def statuscode(self, message): 62 | if "listener" in message: 63 | self.supporter.log(f"Client received status code for handler {message['listener']}: {message['code']}") 64 | 65 | # Check if the username was set 66 | if (message["listener"] == "username_set") and (message["code"] == self.supporter.codes["OK"]): 67 | self.cloudlink.myClientObject = message["val"] 68 | self.supporter.log(f"Client received it's user object: {self.cloudlink.myClientObject}") 69 | 70 | # Check if a ping was successful 71 | if (message["listener"] == "ping_handler") and (message["code"] == self.supporter.codes["OK"]): 72 | self.supporter.log("Last automatic ping return was successful!") 73 | 74 | else: 75 | self.supporter.log(f"Client received status code: {message['code']}") 76 | 77 | # Fire callbacks 78 | if self.statuscode in self.cloudlink.usercallbacks: 79 | if self.cloudlink.usercallbacks[self.statuscode] != None: 80 | self.cloudlink.usercallbacks[self.statuscode](message["code"]) 81 | 82 | # Global messages 83 | def gmsg(self, message): 84 | self.supporter.log(f"Client received global message with data \"{message['val']}\"") 85 | 86 | # Fire callbacks 87 | if self.gmsg in self.cloudlink.usercallbacks: 88 | if self.cloudlink.usercallbacks[self.gmsg] != None: 89 | self.cloudlink.usercallbacks[self.gmsg](message["val"]) 90 | 91 | # Global cloud variables 92 | def gvar(self, message): 93 | self.supporter.log(f"Client received global variable with data \"{message['val']}\"") 94 | 95 | # Fire callbacks 96 | if self.gvar in self.cloudlink.usercallbacks: 97 | if self.cloudlink.usercallbacks[self.gvar] != None: 98 | self.cloudlink.usercallbacks[self.gvar](var_name=message["name"], var_value=message["val"]) 99 | 100 | # Private cloud variables 101 | def pvar(self, message): 102 | self.supporter.log(f"Client received private message with data \"{message['val']}\"") 103 | 104 | # Fire callbacks 105 | if self.pvar in self.cloudlink.usercallbacks: 106 | if self.cloudlink.usercallbacks[self.pvar] != None: 107 | self.cloudlink.usercallbacks[self.pvar](var_name=message["name"], var_value=message["val"], origin=message["origin"]) 108 | 109 | # Private messages 110 | def pmsg(self, message): 111 | self.supporter.log(f"Client received private message with data \"{message['val']}\"") 112 | 113 | # Fire callbacks 114 | if self.pmsg in self.cloudlink.usercallbacks: 115 | if self.cloudlink.usercallbacks[self.pmsg] != None: 116 | self.cloudlink.usercallbacks[self.pmsg](value=message["val"], origin=message["origin"]) 117 | 118 | # Pings 119 | def ping(self, message): 120 | self.supporter.log(f"Client received a ping from {message['origin']}!") 121 | self.cloudlink.sendPacket({"cmd": "statuscode", "val": self.supporter.codes["OK"], "id": message["origin"], "listener": "ping_handler"}) 122 | 123 | # Fire callbacks 124 | if self.ping in self.cloudlink.usercallbacks: 125 | if self.cloudlink.usercallbacks[self.ping] != None: 126 | self.cloudlink.usercallbacks[self.ping](value=message["val"], origin=message["origin"]) 127 | 128 | # WIP 129 | def relay(self, message): 130 | pass -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/client/clientRootHandlers.py: -------------------------------------------------------------------------------- 1 | class clientRootHandlers: 2 | """ 3 | The clientRootHandlers inter class is an interface for the WebsocketClient to communicate with Cloudlink. 4 | Cloudlink.clientRootHandlers.onPacket talks to the Cloudlink.packetHandler function to handle packets, 5 | which will call upon Cloudlink.internalHandlers or some other external, custom code (if used). 6 | """ 7 | 8 | def __init__(self, cloudlink): 9 | self.cloudlink = cloudlink 10 | self.supporter = self.cloudlink.supporter 11 | 12 | def on_error(self, ws, error): 13 | if not error == None: 14 | return 15 | self.supporter.log(f"Client error: {error}") 16 | 17 | # Fire callbacks 18 | if self.on_error in self.cloudlink.usercallbacks: 19 | if self.cloudlink.usercallbacks[self.on_error] != None: 20 | self.cloudlink.usercallbacks[self.on_error](error=error) 21 | 22 | def on_connect(self, ws): 23 | self.supporter.log(f"Client connected.") 24 | self.cloudlink.linkStatus = 2 25 | self.cloudlink.connected = True 26 | 27 | # Fire callbacks 28 | if self.on_connect in self.cloudlink.usercallbacks: 29 | if self.cloudlink.usercallbacks[self.on_connect] != None: 30 | self.cloudlink.usercallbacks[self.on_connect]() 31 | 32 | def on_close(self, ws, close_status_code, close_msg): 33 | if self.cloudlink.linkStatus == 1: 34 | self.cloudlink.linkStatus = 4 35 | self.cloudlink.failedToConnect = True 36 | self.supporter.log(f"Client failed to connect! Disconnected with status code {close_status_code} and message \"{close_msg}\"") 37 | elif self.cloudlink.linkStatus == 2: 38 | self.cloudlink.linkStatus = 4 39 | self.cloudlink.connectionLost = True 40 | self.supporter.log(f"Client lost connection! Disconnected with status code {close_status_code} and message \"{close_msg}\"") 41 | else: 42 | self.cloudlink.linkStatus = 3 43 | self.supporter.log(f"Client gracefully disconnected! Disconnected with status code {close_status_code} and message \"{close_msg}\"") 44 | self.cloudlink.connected = False 45 | 46 | # Fire callbacks 47 | if self.on_close in self.cloudlink.usercallbacks: 48 | if self.cloudlink.usercallbacks[self.on_close] != None: 49 | self.cloudlink.usercallbacks[self.on_close](close_status_code=close_status_code, close_msg=close_msg) 50 | 51 | def on_packet(self, ws, message): 52 | if len(message) != 0: 53 | try: 54 | isPacketSane = self.supporter.isPacketSane(message) 55 | if isPacketSane: 56 | message = self.supporter.json.loads(message) 57 | 58 | # Convert keys in the packet to proper JSON (Primarily for Scratch-based clients) 59 | for key in message.keys(): 60 | if type(message[key]) == str: 61 | if self.supporter.isJSON(message[key]): 62 | message[key] = self.supporter.json.loads(message[key]) 63 | 64 | # Check if the command is a built-in Cloudlink command 65 | if ((message["cmd"] in self.cloudlink.builtInCommands) and not(message["cmd"] == "direct")): 66 | getattr(self.cloudlink, str(message["cmd"]))(message) 67 | # Fire callbacks 68 | if self.on_packet in self.cloudlink.usercallbacks: 69 | if self.cloudlink.usercallbacks[self.on_packet] != None: 70 | self.cloudlink.usercallbacks[self.on_packet](message=message) 71 | else: 72 | # Attempt to read the command as a direct or custom command 73 | isCustom = False 74 | isLegacy = False 75 | isValid = True 76 | if message["cmd"] in self.cloudlink.customCommands: 77 | # New custom command system. 78 | isCustom = True 79 | elif message["cmd"] == "direct": 80 | if self.supporter.isPacketSane(message["val"]): 81 | if type(message["val"]) == dict: 82 | if message["val"]["cmd"] in self.cloudlink.customCommands: 83 | # Legacy custom command system (using direct) 84 | isLegacy = True 85 | else: 86 | isCustom = True 87 | else: 88 | isValid = False 89 | if isValid: 90 | if isLegacy: 91 | self.supporter.log(f"Client recieved legacy custom command \"{message['val']['cmd']}\"") 92 | getattr(self.cloudlink, str(message["val"]["cmd"]))(message=message) 93 | else: 94 | if isCustom: 95 | self.supporter.log(f"Client recieved custom command \"{message['cmd']}\"") 96 | getattr(self.cloudlink, str(message["cmd"]))(message=message) 97 | else: 98 | getattr(self.cloudlink, "direct")(message=message) 99 | 100 | # Fire callbacks 101 | if self.on_packet in self.cloudlink.usercallbacks: 102 | if self.cloudlink.usercallbacks[self.on_packet] != None: 103 | self.cloudlink.usercallbacks[self.on_packet](message=message) 104 | else: 105 | if message["cmd"] in self.cloudlink.disabledCommands: 106 | self.supporter.log(f"Client recieved command \"{message['cmd']}\", but the command is disabled.") 107 | else: 108 | self.supporter.log(f"Client recieved command \"{message['cmd']}\", but the command is invalid or it was not loaded.") 109 | else: 110 | self.supporter.log(f"Packet \"{message}\" is invalid, incomplete, or malformed!") 111 | except: 112 | self.supporter.log(f"An exception has occurred. {self.supporter.full_stack()}") -------------------------------------------------------------------------------- /python/archived/0.1.8.3/cloudlink/client/client.py: -------------------------------------------------------------------------------- 1 | import websocket as WebsocketClient 2 | from .clientRootHandlers import clientRootHandlers 3 | from .clientInternalHandlers import clientInternalHandlers 4 | 5 | class client: 6 | def __init__(self, parentCl, enable_logs=True): 7 | # Read the CloudLink version from the parent class 8 | self.version = parentCl.version 9 | 10 | # Init the client 11 | self.motd_msg = "" 12 | self.sever_version = "" 13 | self.ip_address = "" 14 | self.motd_msg = "" 15 | self.userlist = {} 16 | self.myClientObject = {} 17 | 18 | self.linkStatus = 0 19 | self.failedToConnect = False 20 | self.connectionLost = False 21 | self.connected = False 22 | 23 | # Init modules 24 | self.supporter = parentCl.supporter(self, enable_logs, 2) 25 | self.clientRootHandlers = clientRootHandlers(self) 26 | self.clientInternalHandlers = clientInternalHandlers(self) 27 | 28 | # Load built-in commands (automatically generates attributes for callbacks) 29 | self.builtInCommands = [] 30 | self.customCommands = [] 31 | self.disabledCommands = [] 32 | self.usercallbacks = {} 33 | self.supporter.loadBuiltinCommands(self.clientInternalHandlers) 34 | 35 | # Create API 36 | self.loadCustomCommands = self.supporter.loadCustomCommands 37 | self.disableCommands = self.supporter.disableCommands 38 | self.sendPacket = self.supporter.sendPacket 39 | self.sendCode = self.supporter.sendCode 40 | self.log = self.supporter.log 41 | self.callback = self.supporter.callback 42 | 43 | # Create default callbacks 44 | self.usercallbacks = {} 45 | self.on_packet = self.clientRootHandlers.on_packet 46 | self.on_connect = self.clientRootHandlers.on_connect 47 | self.on_close = self.clientRootHandlers.on_close 48 | self.on_error = self.clientRootHandlers.on_error 49 | 50 | # Callbacks for command-specific events 51 | self.on_direct = self.clientInternalHandlers.direct 52 | self.on_version = self.clientInternalHandlers.server_version 53 | self.on_motd = self.clientInternalHandlers.motd 54 | self.on_ip = self.clientInternalHandlers.client_ip 55 | self.on_ulist = self.clientInternalHandlers.ulist 56 | self.on_statuscode = self.clientInternalHandlers.statuscode 57 | self.on_gmsg = self.clientInternalHandlers.gmsg 58 | self.on_gvar = self.clientInternalHandlers.gvar 59 | self.on_pvar = self.clientInternalHandlers.pvar 60 | self.on_pmsg = self.clientInternalHandlers.pmsg 61 | self.on_ping = self.clientInternalHandlers.ping 62 | 63 | self.log("Cloudlink client initialized!") 64 | 65 | def run(self, ip="ws://127.0.0.1:3000/"): 66 | # Initialize the Websocket client 67 | self.log("Cloudlink client starting up now...") 68 | self.wss = WebsocketClient.WebSocketApp( 69 | ip, 70 | on_message = self.clientRootHandlers.on_packet, 71 | on_error = self.clientRootHandlers.on_error, 72 | on_open = self.clientRootHandlers.on_connect, 73 | on_close = self.clientRootHandlers.on_close 74 | ) 75 | 76 | # Run the CloudLink client 77 | self.linkStatus = 1 78 | self.wss.run_forever() 79 | self.log("Cloudlink client exiting...") 80 | 81 | def stop(self): 82 | if self.connected: 83 | self.linkStatus = 3 84 | self.log("Cloudlink client disconnecting...") 85 | self.wss.close() 86 | 87 | # Client API 88 | 89 | def setUsername(self, username:str): 90 | if self.connected: 91 | msg = {"cmd": "setid", "val": username, "listener": "username_set"} 92 | self.cloudlink.sendPacket(msg) 93 | 94 | def getUserlist(self, listener:str = None): 95 | if self.connected: 96 | msg = {"cmd": "ulist", "val": ""} 97 | if listener: 98 | msg["listener"] = listener 99 | self.cloudlink.sendPacket(msg) 100 | 101 | def linkToRooms(self, rooms:list = ["default"], listener:str = None): 102 | if self.connected: 103 | msg = {"cmd": "link", "val": rooms} 104 | if listener: 105 | msg["listener"] = listener 106 | self.cloudlink.sendPacket(msg) 107 | 108 | def unlinkFromRooms(self, listener:str = None): 109 | if self.connected: 110 | msg = {"cmd": "unlink", "val": ""} 111 | if listener: 112 | msg["listener"] = listener 113 | self.cloudlink.sendPacket(msg) 114 | 115 | def sendDirect(self, message:str, username:str = None, listener:str = None): 116 | if self.connected: 117 | msg = {"cmd": "direct", "val": message} 118 | if listener: 119 | msg["listener"] = listener 120 | if username: 121 | msg["id"] = username 122 | self.cloudlink.sendPacket(msg) 123 | 124 | def sendCustom(self, cmd:str, message:str, username:str = None, listener:str = None): 125 | if self.connected: 126 | msg = {"cmd": cmd, "val": message} 127 | if listener: 128 | msg["listener"] = listener 129 | if username: 130 | msg["id"] = username 131 | self.cloudlink.sendPacket(msg) 132 | 133 | def sendPing(self, dummy_payload:str = "", username:str = None, listener:str = None): 134 | if self.connected: 135 | msg = {"cmd": "ping", "val": dummy_payload} 136 | if listener: 137 | msg["listener"] = listener 138 | if username: 139 | msg["id"] = username 140 | self.cloudlink.sendPacket(msg) 141 | 142 | def sendGlobalMessage(self, message:str, listener:str = None): 143 | if self.connected: 144 | msg = {"cmd": "gmsg", "val": message} 145 | if listener: 146 | msg["listener"] = listener 147 | self.cloudlink.sendPacket(msg) 148 | 149 | def sendPrivateMessage(self, message:str, username:str = "", listener:str = None): 150 | if self.connected: 151 | msg = {"cmd": "pmsg", "val": message, "id": username} 152 | if listener: 153 | msg["listener"] = listener 154 | self.cloudlink.sendPacket(msg) 155 | 156 | def sendGlobalVariable(self, var_name:str, var_value:str, listener:str = None): 157 | if self.connected: 158 | msg = {"cmd": "gvar", "val": var_value, "name": var_name} 159 | if listener: 160 | msg["listener"] = listener 161 | self.cloudlink.sendPacket(msg) 162 | 163 | def sendPrivateVariable(self, var_name:str, var_value:str, username:str = "", listener:str = None): 164 | if self.connected: 165 | msg = {"cmd": "pvar", "val": var_value, "name": var_name, "id": username} 166 | if listener: 167 | msg["listener"] = listener 168 | self.cloudlink.sendPacket(msg) -------------------------------------------------------------------------------- /python/cloudlink/client/protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the default protocol used for the CloudLink client. 3 | The CloudLink 4.1 Protocol retains full support for CLPv4. 4 | 5 | Each packet format is compliant with UPLv2 formatting rules. 6 | 7 | Documentation for the CLPv4.1 protocol can be found here: 8 | https://hackmd.io/@MikeDEV/HJiNYwOfo 9 | """ 10 | 11 | 12 | class clpv4: 13 | def __init__(self, parent): 14 | 15 | # Define various status codes for the protocol. 16 | class statuscodes: 17 | # Code type character 18 | info = "I" 19 | error = "E" 20 | 21 | # Error / info codes as tuples 22 | test = (info, 0, "Test") 23 | echo = (info, 1, "Echo") 24 | ok = (info, 100, "OK") 25 | syntax = (error, 101, "Syntax") 26 | datatype = (error, 102, "Datatype") 27 | id_not_found = (error, 103, "ID not found") 28 | id_not_specific = (error, 104, "ID not specific enough") 29 | internal_error = (error, 105, "Internal server error") 30 | empty_packet = (error, 106, "Empty packet") 31 | id_already_set = (error, 107, "ID already set") 32 | refused = (error, 108, "Refused") 33 | invalid_command = (error, 109, "Invalid command") 34 | disabled_command = (error, 110, "Command disabled") 35 | id_required = (error, 111, "ID required") 36 | id_conflict = (error, 112, "ID conflict") 37 | too_large = (error, 113, "Too large") 38 | json_error = (error, 114, "JSON error") 39 | room_not_joined = (error, 115, "Room not joined") 40 | 41 | # Generate a user object 42 | def generate_user_object(): 43 | # Username set 44 | if parent.client.username_set: 45 | return { 46 | "id": parent.client.snowflake, 47 | "username": parent.client.username, 48 | "uuid": str(parent.client.id) 49 | } 50 | 51 | # Username not set 52 | return { 53 | "id": parent.client.snowflake, 54 | "uuid": str(parent.client.id) 55 | } 56 | 57 | # Expose username object generator function for extension usage 58 | self.generate_user_object = generate_user_object 59 | 60 | async def set_username(username): 61 | parent.logger.debug(f"Setting username to {username}...") 62 | 63 | # Send the set username request with a listener and wait for a response 64 | response = await parent.send_packet_and_wait({ 65 | "cmd": "setid", 66 | "val": username, 67 | "listener": "init_username" 68 | }) 69 | 70 | if response["code_id"] == statuscodes.ok[1]: 71 | # Log the successful connection 72 | parent.logger.info(f"Successfully set username to {username}.") 73 | 74 | # Fire all on_connect events 75 | val = response["val"] 76 | parent.asyncio.create_task( 77 | parent.execute_on_username_set_events(val["id"], val["username"], val["uuid"]) 78 | ) 79 | 80 | else: 81 | # Log the connection error 82 | parent.logger.error(f"Failed to set username. Got response code: {response['code']}") 83 | 84 | # Expose the username set command 85 | self.set_username = set_username 86 | 87 | # The CLPv4 command set 88 | @parent.on_initial_connect 89 | async def on_initial_connect(): 90 | parent.logger.debug("Performing handshake with the server...") 91 | 92 | # Send the handshake request with a listener and wait for a response 93 | response = await parent.send_packet_and_wait({ 94 | "cmd": "handshake", 95 | "val": { 96 | "language": "Python", 97 | "version": parent.version 98 | }, 99 | "listener": "init_handshake" 100 | }) 101 | 102 | if response["code_id"] == statuscodes.ok[1]: 103 | # Log the successful connection 104 | parent.logger.info("Successfully connected to the server.") 105 | 106 | # Fire all on_connect events 107 | parent.asyncio.create_task( 108 | parent.execute_on_full_connect_events() 109 | ) 110 | 111 | else: 112 | # Log the connection error 113 | parent.logger.error(f"Failed to connect to the server. Got response code: {response['code']}") 114 | 115 | # Disconnect 116 | parent.asyncio.create_task( 117 | parent.disconnect() 118 | ) 119 | 120 | @parent.on_command(cmd="ping") 121 | async def on_ping(message): 122 | events = [event(message) for event in parent.protocol_command_handlers["ping"]] 123 | group = parent.asyncio.gather(*events) 124 | await group 125 | 126 | @parent.on_command(cmd="gmsg") 127 | async def on_gmsg(message): 128 | events = [event(message) for event in parent.protocol_command_handlers["gmsg"]] 129 | group = parent.asyncio.gather(*events) 130 | await group 131 | 132 | @parent.on_command(cmd="pmsg") 133 | async def on_pmsg(message): 134 | events = [event(message) for event in parent.protocol_command_handlers["pmsg"]] 135 | group = parent.asyncio.gather(*events) 136 | await group 137 | 138 | @parent.on_command(cmd="gvar") 139 | async def on_gvar(message): 140 | events = [event(message) for event in parent.protocol_command_handlers["gvar"]] 141 | group = parent.asyncio.gather(*events) 142 | await group 143 | 144 | @parent.on_command(cmd="pvar") 145 | async def on_pvar(message): 146 | events = [event(message) for event in parent.protocol_command_handlers["pvar"]] 147 | group = parent.asyncio.gather(*events) 148 | await group 149 | 150 | @parent.on_command(cmd="statuscode") 151 | async def on_statuscode(message): 152 | events = [event(message) for event in parent.protocol_command_handlers["statuscode"]] 153 | group = parent.asyncio.gather(*events) 154 | await group 155 | 156 | @parent.on_command(cmd="client_obj") 157 | async def on_client_obj(message): 158 | parent.logger.info(f"This client is known as ID {message['val']['id']} with UUID {message['val']['uuid']}.") 159 | 160 | @parent.on_command(cmd="client_ip") 161 | async def on_client_ip(message): 162 | parent.logger.debug(f"Client IP address is {message['val']}") 163 | 164 | @parent.on_command(cmd="server_version") 165 | async def on_server_version(message): 166 | parent.logger.info(f"Server is running Cloudlink v{message['val']}.") 167 | 168 | @parent.on_command(cmd="ulist") 169 | async def on_ulist(message): 170 | events = [event(message) for event in parent.protocol_command_handlers["ulist"]] 171 | group = parent.asyncio.gather(*events) 172 | await group 173 | 174 | @parent.on_command(cmd="direct") 175 | async def on_direct(message): 176 | events = [event(message) for event in parent.protocol_command_handlers["direct"]] 177 | group = parent.asyncio.gather(*events) 178 | await group 179 | -------------------------------------------------------------------------------- /python/cloudlink/server/protocols/clpv4/schema.py: -------------------------------------------------------------------------------- 1 | # Schema for interpreting the Cloudlink protocol v4.0 (CLPv4) command set 2 | class cl4_protocol: 3 | 4 | # Required - Defines the keyword to use to define the command 5 | command_key = "cmd" 6 | 7 | # Required - Defines the default schema to test against 8 | default = { 9 | "cmd": { 10 | "type": "string", 11 | "required": True 12 | }, 13 | "val": { 14 | "type": [ 15 | "string", 16 | "integer", 17 | "float", 18 | "number", 19 | "boolean", 20 | "dict", 21 | "list", 22 | "set", 23 | ], 24 | "required": False, 25 | }, 26 | "name": { 27 | "type": "string", 28 | "required": False 29 | }, 30 | "id": { 31 | "type": [ 32 | "string", 33 | "dict", 34 | "list", 35 | "set" 36 | ], 37 | "required": False 38 | }, 39 | "listener": { 40 | "type": [ 41 | "string", 42 | "integer", 43 | "float", 44 | "boolean", 45 | "number" 46 | ], 47 | "required": False 48 | }, 49 | "rooms": { 50 | "type": [ 51 | "string", 52 | "integer", 53 | "float", 54 | "boolean", 55 | "number", 56 | "list", 57 | "set" 58 | ], 59 | "required": False 60 | } 61 | } 62 | 63 | linking = { 64 | "cmd": { 65 | "type": "string", 66 | "required": True 67 | }, 68 | "val": { 69 | "type": [ 70 | "string", 71 | "integer", 72 | "float", 73 | "number", 74 | "boolean", 75 | "dict", 76 | "list", 77 | "set", 78 | ], 79 | "required": True, 80 | }, 81 | "listener": { 82 | "type": [ 83 | "string", 84 | "integer", 85 | "float", 86 | "boolean", 87 | "number" 88 | ], 89 | "required": False 90 | } 91 | } 92 | 93 | setid = { 94 | "cmd": { 95 | "type": "string", 96 | "required": True 97 | }, 98 | "val": { 99 | "type": "string", 100 | "required": True 101 | }, 102 | "listener": { 103 | "type": [ 104 | "string", 105 | "integer", 106 | "float", 107 | "boolean", 108 | "number" 109 | ], 110 | "required": False 111 | } 112 | } 113 | 114 | gmsg = { 115 | "cmd": { 116 | "type": "string", 117 | "required": True 118 | }, 119 | "val": { 120 | "type": [ 121 | "string", 122 | "integer", 123 | "float", 124 | "number", 125 | "boolean", 126 | "dict", 127 | "list", 128 | "set", 129 | ], 130 | "required": True 131 | }, 132 | "listener": { 133 | "type": [ 134 | "string", 135 | "integer", 136 | "float", 137 | "boolean", 138 | "number" 139 | ], 140 | "required": False 141 | }, 142 | "rooms": { 143 | "type": [ 144 | "string", 145 | "integer", 146 | "float", 147 | "boolean", 148 | "number", 149 | "list", 150 | "set" 151 | ], 152 | "required": False 153 | } 154 | } 155 | 156 | gvar = { 157 | "cmd": { 158 | "type": "string", 159 | "required": True 160 | }, 161 | "name": { 162 | "type": "string", 163 | "required": True 164 | }, 165 | "val": { 166 | "type": [ 167 | "string", 168 | "integer", 169 | "float", 170 | "number", 171 | "boolean", 172 | "dict", 173 | "list", 174 | "set", 175 | ], 176 | "required": True 177 | }, 178 | "listener": { 179 | "type": [ 180 | "string", 181 | "integer", 182 | "float", 183 | "boolean", 184 | "number" 185 | ], 186 | "required": False 187 | }, 188 | "rooms": { 189 | "type": [ 190 | "string", 191 | "integer", 192 | "float", 193 | "boolean", 194 | "number", 195 | "list", 196 | "set" 197 | ], 198 | "required": False 199 | } 200 | } 201 | 202 | pmsg = { 203 | "cmd": { 204 | "type": "string", 205 | "required": True 206 | }, 207 | "id": { 208 | "type": [ 209 | "string", 210 | "dict", 211 | "list", 212 | "set" 213 | ], 214 | "required": True 215 | }, 216 | "val": { 217 | "type": [ 218 | "string", 219 | "integer", 220 | "float", 221 | "number", 222 | "boolean", 223 | "dict", 224 | "list", 225 | "set", 226 | ], 227 | "required": True 228 | }, 229 | "listener": { 230 | "type": [ 231 | "string", 232 | "integer", 233 | "float", 234 | "boolean", 235 | "number" 236 | ], 237 | "required": False 238 | }, 239 | "rooms": { 240 | "type": [ 241 | "string", 242 | "integer", 243 | "float", 244 | "boolean", 245 | "number", 246 | "list", 247 | "set" 248 | ], 249 | "required": False 250 | } 251 | } 252 | 253 | direct = { 254 | "cmd": { 255 | "type": "string", 256 | "required": True 257 | }, 258 | "id": { 259 | "type": "string", 260 | "required": True 261 | }, 262 | "val": { 263 | "type": [ 264 | "string", 265 | "integer", 266 | "float", 267 | "number", 268 | "boolean", 269 | "dict", 270 | "list", 271 | "set", 272 | ], 273 | "required": True 274 | }, 275 | "listener": { 276 | "type": [ 277 | "string", 278 | "integer", 279 | "float", 280 | "boolean", 281 | "number" 282 | ], 283 | "required": False 284 | } 285 | } 286 | 287 | pvar = { 288 | "cmd": { 289 | "type": "string", 290 | "required": True 291 | }, 292 | "name": { 293 | "type": "string", 294 | "required": True 295 | }, 296 | "id": { 297 | "type": [ 298 | "string", 299 | "dict", 300 | "list", 301 | "set" 302 | ], 303 | "required": True 304 | }, 305 | "val": { 306 | "type": [ 307 | "string", 308 | "integer", 309 | "float", 310 | "number", 311 | "boolean", 312 | "dict", 313 | "list", 314 | "set", 315 | ], 316 | "required": True 317 | }, 318 | "listener": { 319 | "type": [ 320 | "string", 321 | "integer", 322 | "float", 323 | "boolean", 324 | "number" 325 | ], 326 | "required": False 327 | }, 328 | "rooms": { 329 | "type": [ 330 | "string", 331 | "integer", 332 | "float", 333 | "boolean", 334 | "number", 335 | "list", 336 | "set" 337 | ], 338 | "required": False 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /python/archived/0.1.8.0/cloudlink/serverRootHandlers.py: -------------------------------------------------------------------------------- 1 | class serverRootHandlers: 2 | """ 3 | The serverRootHandlers inter class is an interface for the WebsocketServer to communicate with Cloudlink. 4 | Cloudlink.serverRootHandlers.onPacket talks to the Cloudlink.packetHandler function to handle packets, 5 | which will call upon Cloudlink.internalHandlers or some other external, custom code (if used). 6 | """ 7 | 8 | def __init__(self, cloudlink): 9 | self.cloudlink = cloudlink 10 | self.supporter = self.cloudlink.supporter 11 | 12 | def on_connect(self, client, server): 13 | if not(client == None): 14 | if self.cloudlink.rejectClientMode: 15 | self.supporter.log(f"Client {client['id']} ({client['address']}) was rejected.") 16 | 17 | # Reject the client 18 | self.supporter.rejectClient(client, "Connection rejected") 19 | 20 | elif self.supporter.getFriendlyClientIP(client) in self.cloudlink.ipblocklist: 21 | self.supporter.log(f"Client {client['id']} ({client['address']}) was blocked from connecting.") 22 | 23 | # Block the client 24 | self.supporter.rejectClient(client, "IP blocked") 25 | else: 26 | self.supporter.log(f"Client {client['id']} ({client['address']}) connected.") 27 | 28 | # Create attributes for the client 29 | self.supporter.createAttrForClient(client) 30 | 31 | # Report to the client it's IP address 32 | self.cloudlink.sendPacket(client, {"cmd": "client_ip", "val": str(client["address"])}) 33 | 34 | # Report to the client the CL Server version 35 | self.cloudlink.sendPacket(client, {"cmd": "server_version", "val": str(self.cloudlink.version)}) 36 | 37 | # Report to the client the currently cached global message 38 | self.cloudlink.sendPacket(client, {"cmd": "gmsg", "val": self.cloudlink.global_msg}) 39 | 40 | # Update all clients with the updated userlist 41 | self.cloudlink.sendPacket(self.cloudlink.all_clients, {"cmd": "ulist", "val": self.supporter.getUsernames()}) 42 | 43 | # Tell the client the server's Message-Of-The-Day (MOTD) 44 | if self.cloudlink.motd_enable: 45 | self.cloudlink.sendPacket(client, {"cmd": "motd", "val": self.cloudlink.motd_msg}) 46 | 47 | if self.on_connect in self.cloudlink.usercallbacks: 48 | if self.cloudlink.usercallbacks[self.on_connect] != None: 49 | self.cloudlink.usercallbacks[self.on_connect](client=client, server=server) 50 | 51 | def on_close(self, client, server): 52 | if not(client == None): 53 | self.supporter.log(f"Client {client['id']} ({client['address']}) disconnected.") 54 | self.supporter.deleteAttrForClient(client) 55 | 56 | if self.on_close in self.cloudlink.usercallbacks: 57 | if self.cloudlink.usercallbacks[self.on_close] != None: 58 | self.cloudlink.usercallbacks[self.on_close](client=client, server=server) 59 | 60 | def on_packet(self, client, server, message): 61 | if not(client == None): 62 | if len(message) == 0: 63 | self.supporter.log(f"Packet from {client['id']} was blank!") 64 | self.cloudlink.sendCode(client, "EmptyPacket") 65 | else: 66 | try: 67 | isPacketSane = self.supporter.isPacketSane(message) 68 | if isPacketSane: 69 | message = self.supporter.json.loads(message) 70 | 71 | # Convert keys in the packet to proper JSON (Primarily for Scratch-based clients) 72 | for key in message.keys(): 73 | if type(message[key]) == str: 74 | if self.supporter.isJSON(message[key]): 75 | message[key] = self.supporter.json.loads(message[key]) 76 | 77 | # Check if the packet contains a listener ID 78 | listener_detected = ("listener" in message) 79 | listener_id = None 80 | if listener_detected: 81 | listener_id = message["listener"] 82 | else: 83 | # Check for old layout (Mainly for Direct command) listeners 84 | listener_detected = ("listener" in message["val"]) 85 | if listener_detected: 86 | listener_id = message["val"]["listener"] 87 | 88 | room_id = None 89 | if self.supporter.readAttrFromClient(client)["is_linked"]: 90 | room_id = self.supporter.readAttrFromClient(client)["rooms"] 91 | 92 | if "room" in message: 93 | if type(message["room"]) in [str, list]: 94 | # Convert to list 95 | if type(message["room"]) == str: 96 | message["room"] = [message["room"]] 97 | 98 | room_id = message["room"] 99 | 100 | # Check if the command is a built-in Cloudlink command 101 | if ((message["cmd"] in self.cloudlink.builtInCommands) and not(message["cmd"] == "direct")): 102 | getattr(self.cloudlink, str(message["cmd"]))(client, server, message, listener_detected, listener_id, room_id) 103 | else: 104 | # Attempt to read the command as a direct or custom command 105 | isCustom = False 106 | isLegacy = False 107 | isValid = True 108 | if message["cmd"] in self.cloudlink.customCommands: 109 | # New custom command system. 110 | isCustom = True 111 | elif message["cmd"] == "direct": 112 | if self.supporter.isPacketSane(message["val"]): 113 | if type(message["val"]) == dict: 114 | if message["val"]["cmd"] in self.cloudlink.customCommands: 115 | # Legacy custom command system (using direct) 116 | isLegacy = True 117 | else: 118 | isCustom = True 119 | else: 120 | isValid = False 121 | if isValid: 122 | if isLegacy: 123 | self.supporter.log(f"Client {client['id']} ({client['address']}) sent legacy custom command \"{message['val']['cmd']}\"") 124 | getattr(self.cloudlink, str(message["val"]["cmd"]))(client, server, message, listener_detected, listener_id, room_id) 125 | else: 126 | if isCustom: 127 | self.supporter.log(f"Client {client['id']} ({client['address']}) sent custom command \"{message['cmd']}\"") 128 | getattr(self.cloudlink, str(message["cmd"]))(client, server, message, listener_detected, listener_id, room_id) 129 | else: 130 | getattr(self.cloudlink, "direct")(client, server, message, listener_detected, listener_id, room_id) 131 | else: 132 | if message["cmd"] in self.cloudlink.disabledCommands: 133 | self.supporter.log(f"Client {client['id']} ({client['address']}) sent custom command \"{message['cmd']}\", but the command is disabled.") 134 | self.cloudlink.sendCode(client, "Disabled") 135 | else: 136 | self.supporter.log(f"Client {client['id']} ({client['address']}) sent custom command \"{message['cmd']}\", but the command is invalid or it was not loaded.") 137 | self.cloudlink.sendCode(client, "Invalid") 138 | 139 | if self.on_packet in self.cloudlink.usercallbacks: 140 | if self.cloudlink.usercallbacks[self.on_packet] != None: 141 | self.cloudlink.usercallbacks[self.on_packet](client=client, server=server, message=message) 142 | else: 143 | self.supporter.log(f"Packet \"{message}\" is invalid, incomplete, or malformed!") 144 | self.cloudlink.sendCode(client, "Syntax") 145 | except: 146 | self.supporter.log(f"An exception has occurred. {self.supporter.full_stack()}") 147 | self.cloudlink.sendCode(client, "InternalServerError") -------------------------------------------------------------------------------- /python/cloudlink/client/schema.py: -------------------------------------------------------------------------------- 1 | # Schema for interpreting the Cloudlink protocol v4.0 (CLPv4) command set 2 | class schema: 3 | 4 | # Required - Defines the keyword to use to define the command 5 | command_key = "cmd" 6 | 7 | # Required - Defines the default schema to test against 8 | default = { 9 | "cmd": { 10 | "type": "string", 11 | "required": True 12 | }, 13 | "val": { 14 | "type": [ 15 | "string", 16 | "integer", 17 | "float", 18 | "number", 19 | "boolean", 20 | "dict", 21 | "list", 22 | "set", 23 | ], 24 | "required": False, 25 | }, 26 | "mode": { 27 | "type": "string", 28 | "required": False 29 | }, 30 | "code": { 31 | "type": "string", 32 | "required": False 33 | }, 34 | "code_id": { 35 | "type": "integer", 36 | "required": False 37 | }, 38 | "name": { 39 | "type": "string", 40 | "required": False 41 | }, 42 | "id": { 43 | "type": [ 44 | "string", 45 | "dict", 46 | "list", 47 | "set" 48 | ], 49 | "required": False 50 | }, 51 | "listener": { 52 | "type": [ 53 | "string", 54 | "integer", 55 | "float", 56 | "boolean", 57 | "number" 58 | ], 59 | "required": False 60 | }, 61 | "rooms": { 62 | "type": [ 63 | "string", 64 | "integer", 65 | "float", 66 | "boolean", 67 | "number", 68 | "list", 69 | "set" 70 | ], 71 | "required": False 72 | } 73 | } 74 | 75 | linking = { 76 | "cmd": { 77 | "type": "string", 78 | "required": True 79 | }, 80 | "val": { 81 | "type": [ 82 | "string", 83 | "integer", 84 | "float", 85 | "number", 86 | "boolean", 87 | "dict", 88 | "list", 89 | "set", 90 | ], 91 | "required": True, 92 | }, 93 | "listener": { 94 | "type": [ 95 | "string", 96 | "integer", 97 | "float", 98 | "boolean", 99 | "number" 100 | ], 101 | "required": False 102 | } 103 | } 104 | 105 | setid = { 106 | "cmd": { 107 | "type": "string", 108 | "required": True 109 | }, 110 | "val": { 111 | "type": "string", 112 | "required": True 113 | }, 114 | "listener": { 115 | "type": [ 116 | "string", 117 | "integer", 118 | "float", 119 | "boolean", 120 | "number" 121 | ], 122 | "required": False 123 | } 124 | } 125 | 126 | gmsg = { 127 | "cmd": { 128 | "type": "string", 129 | "required": True 130 | }, 131 | "val": { 132 | "type": [ 133 | "string", 134 | "integer", 135 | "float", 136 | "number", 137 | "boolean", 138 | "dict", 139 | "list", 140 | "set", 141 | ], 142 | "required": True 143 | }, 144 | "listener": { 145 | "type": [ 146 | "string", 147 | "integer", 148 | "float", 149 | "boolean", 150 | "number" 151 | ], 152 | "required": False 153 | }, 154 | "rooms": { 155 | "type": [ 156 | "string", 157 | "integer", 158 | "float", 159 | "boolean", 160 | "number", 161 | "list", 162 | "set" 163 | ], 164 | "required": False 165 | } 166 | } 167 | 168 | gvar = { 169 | "cmd": { 170 | "type": "string", 171 | "required": True 172 | }, 173 | "name": { 174 | "type": "string", 175 | "required": True 176 | }, 177 | "val": { 178 | "type": [ 179 | "string", 180 | "integer", 181 | "float", 182 | "number", 183 | "boolean", 184 | "dict", 185 | "list", 186 | "set", 187 | ], 188 | "required": True 189 | }, 190 | "listener": { 191 | "type": [ 192 | "string", 193 | "integer", 194 | "float", 195 | "boolean", 196 | "number" 197 | ], 198 | "required": False 199 | }, 200 | "rooms": { 201 | "type": [ 202 | "string", 203 | "integer", 204 | "float", 205 | "boolean", 206 | "number", 207 | "list", 208 | "set" 209 | ], 210 | "required": False 211 | } 212 | } 213 | 214 | pmsg = { 215 | "cmd": { 216 | "type": "string", 217 | "required": True 218 | }, 219 | "id": { 220 | "type": [ 221 | "string", 222 | "dict", 223 | "list", 224 | "set" 225 | ], 226 | "required": True 227 | }, 228 | "val": { 229 | "type": [ 230 | "string", 231 | "integer", 232 | "float", 233 | "number", 234 | "boolean", 235 | "dict", 236 | "list", 237 | "set", 238 | ], 239 | "required": True 240 | }, 241 | "listener": { 242 | "type": [ 243 | "string", 244 | "integer", 245 | "float", 246 | "boolean", 247 | "number" 248 | ], 249 | "required": False 250 | }, 251 | "rooms": { 252 | "type": [ 253 | "string", 254 | "integer", 255 | "float", 256 | "boolean", 257 | "number", 258 | "list", 259 | "set" 260 | ], 261 | "required": False 262 | } 263 | } 264 | 265 | direct = { 266 | "cmd": { 267 | "type": "string", 268 | "required": True 269 | }, 270 | "id": { 271 | "type": "string", 272 | "required": True 273 | }, 274 | "val": { 275 | "type": [ 276 | "string", 277 | "integer", 278 | "float", 279 | "number", 280 | "boolean", 281 | "dict", 282 | "list", 283 | "set", 284 | ], 285 | "required": True 286 | }, 287 | "listener": { 288 | "type": [ 289 | "string", 290 | "integer", 291 | "float", 292 | "boolean", 293 | "number" 294 | ], 295 | "required": False 296 | } 297 | } 298 | 299 | pvar = { 300 | "cmd": { 301 | "type": "string", 302 | "required": True 303 | }, 304 | "name": { 305 | "type": "string", 306 | "required": True 307 | }, 308 | "id": { 309 | "type": [ 310 | "string", 311 | "dict", 312 | "list", 313 | "set" 314 | ], 315 | "required": True 316 | }, 317 | "val": { 318 | "type": [ 319 | "string", 320 | "integer", 321 | "float", 322 | "number", 323 | "boolean", 324 | "dict", 325 | "list", 326 | "set", 327 | ], 328 | "required": True 329 | }, 330 | "listener": { 331 | "type": [ 332 | "string", 333 | "integer", 334 | "float", 335 | "boolean", 336 | "number" 337 | ], 338 | "required": False 339 | }, 340 | "rooms": { 341 | "type": [ 342 | "string", 343 | "integer", 344 | "float", 345 | "boolean", 346 | "number", 347 | "list", 348 | "set" 349 | ], 350 | "required": False 351 | } 352 | } -------------------------------------------------------------------------------- /python/archived/0.1.9.1/cloudlink/supporter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | from datetime import datetime 4 | 5 | 6 | class supporter: 7 | def __init__(self, parent): 8 | self.parent = parent 9 | 10 | # Define protocol types 11 | self.proto_unset = "proto_unset" 12 | self.proto_cloudlink = "proto_cloudlink" 13 | self.proto_scratch_cloud = "proto_scratch_cloud" 14 | 15 | # Multicasting message quirks 16 | self.quirk_embed_val = "quirk_embed_val" 17 | self.quirk_update_msg = "quirk_update_msg" 18 | 19 | # Case codes 20 | self.valid = 0 21 | self.invalid = 1 22 | self.missing_key = 2 23 | self.not_a_dict = 3 24 | self.unknown_method = 4 25 | self.unknown_protocol = 5 26 | self.username_set = 6 27 | self.username_not_set = 7 28 | self.disabled_method = 8 29 | self.too_large = 9 30 | 31 | # Scratch error codes 32 | self.connection_error = 4000 33 | self.username_error = 4002 34 | self.overloaded = 4003 35 | self.unavailable = 4004 36 | self.refused_security = 4005 37 | 38 | # Status codes 39 | self.info = "I" 40 | self.error = "E" 41 | self.codes = { 42 | "Test": (self.info, 0, "Test"), 43 | "OK": (self.info, 100, "OK"), 44 | "Syntax": (self.error, 101, "Syntax"), 45 | "DataType": (self.error, 102, "Datatype"), 46 | "IDNotFound": (self.error, 103, "ID not found"), 47 | "IDNotSpecific": (self.error, 104, "ID not specific enough"), 48 | "InternalServerError": (self.error, 105, "Internal server error"), 49 | "EmptyPacket": (self.error, 106, "Empty packet"), 50 | "IDSet": (self.error, 107, "ID already set"), 51 | "Refused": (self.error, 108, "Refused"), 52 | "Invalid": (self.error, 109, "Invalid command"), 53 | "Disabled": (self.error, 110, "Command disabled"), 54 | "IDRequired": (self.error, 111, "ID required"), 55 | "IDConflict": (self.error, 112, "ID conflict"), 56 | "TooLarge": (self.error, 113, "Too large") 57 | } 58 | 59 | # Method default keys and permitted datatypes 60 | self.keydefaults = { 61 | "val": [str, int, float, dict], 62 | "id": [str, int, dict, set, list], 63 | "listener": [str, dict, float, int], 64 | "rooms": [str, list], 65 | "name": str, 66 | "user": str, 67 | "project_id": str, 68 | "method": str, 69 | "cmd": str, 70 | "value": [str, int, float] 71 | } 72 | 73 | # New and improved version of the message sanity checker. 74 | def validate(self, keys: dict, payload: dict, optional=None, sizes: dict = None): 75 | # Check if input datatypes are valid 76 | if optional is None: 77 | optional = [] 78 | if (type(keys) != dict) or (type(payload) != dict): 79 | return self.not_a_dict 80 | 81 | for key in keys.keys(): 82 | # Check if a key is present 83 | if (key in payload) or (key in optional): 84 | # Bypass checks if a key is optional and not present 85 | if (key not in payload) and (key in optional): 86 | continue 87 | 88 | # Check if there are multiple supported datatypes for a key 89 | if type(keys[key]) == list: 90 | # Validate key datatype 91 | if not type(payload[key]) in keys[key]: 92 | return self.invalid 93 | 94 | # Check if the size of the payload is too large 95 | if sizes: 96 | if (key in sizes.keys()) and (len(str(payload[key])) > sizes[key]): 97 | return self.too_large 98 | 99 | else: 100 | # Validate key datatype 101 | if type(payload[key]) != keys[key]: 102 | return self.invalid 103 | 104 | # Check if the size of the payload is too large 105 | if sizes: 106 | if (key in sizes.keys()) and (len(str(payload[key])) > sizes[key]): 107 | return self.too_large 108 | else: 109 | return self.missing_key 110 | 111 | # Hooray, the message is sane 112 | return self.valid 113 | 114 | def full_stack(self): 115 | exc = sys.exc_info()[0] 116 | if exc is not None: 117 | f = sys.exc_info()[-1].tb_frame.f_back 118 | stack = traceback.extract_stack(f) 119 | else: 120 | stack = traceback.extract_stack()[:-1] 121 | trc = 'Traceback (most recent call last):\n' 122 | stackstr = trc + ''.join(traceback.format_list(stack)) 123 | if exc is not None: 124 | stackstr += ' ' + traceback.format_exc().lstrip(trc) 125 | return stackstr 126 | 127 | def is_json(self, json_str): 128 | is_valid_json = False 129 | try: 130 | if type(json_str) == dict: 131 | is_valid_json = True 132 | elif type(json_str) == str: 133 | json_str = self.json.loads(json_str) 134 | is_valid_json = True 135 | except: 136 | is_valid_json = False 137 | return is_valid_json 138 | 139 | def get_client_ip(self, client: dict): 140 | if "x-forwarded-for" in client.request_headers: 141 | return client.request_headers.get("x-forwarded-for") 142 | elif "cf-connecting-ip" in client.request_headers: 143 | return client.request_headers.get("cf-connecting-ip") 144 | else: 145 | if type(client.remote_address) == tuple: 146 | return str(client.remote_address[0]) 147 | else: 148 | return client.remote_address 149 | 150 | def generate_statuscode(self, code: str): 151 | if code in self.codes: 152 | c_type, c_code, c_msg = self.codes[code] 153 | return f"{c_type}:{c_code} | {c_msg}", c_code 154 | else: 155 | raise ValueError 156 | 157 | # Determines if a method 158 | def detect_listener(self, message): 159 | validation = self.validate( 160 | { 161 | "listener": self.keydefaults["listener"] 162 | }, 163 | message 164 | ) 165 | 166 | match validation: 167 | case self.invalid: 168 | return None 169 | case self.missing_key: 170 | return None 171 | 172 | return message["listener"] 173 | 174 | # Internal usage only, not for use in Public API 175 | def get_rooms(self, client, message): 176 | rooms = set() 177 | if "rooms" not in message: 178 | rooms.update(client.rooms) 179 | else: 180 | if type(message["rooms"]) == str: 181 | message["rooms"] = [message["rooms"]] 182 | rooms.update(set(message["rooms"])) 183 | 184 | # Filter rooms client doesn't have access to 185 | for room in self.copy(rooms): 186 | if room not in client.rooms: 187 | rooms.remove(room) 188 | return rooms 189 | 190 | # Disables methods. Supports disabling built-in methods for monkey-patching or for custom reimplementation. 191 | def disable_methods(self, functions: list): 192 | if type(functions) != list: 193 | raise TypeError 194 | 195 | for function in functions: 196 | if type(function) != str: 197 | continue 198 | 199 | if function not in self.parent.disabled_methods: 200 | self.parent.disabled_methods.add(function) 201 | 202 | self.parent.safe_methods.discard(function) 203 | 204 | # Support for loading custom methods. Automatically selects safe methods. 205 | def load_custom_methods(self, _class): 206 | for function in dir(_class): 207 | # Ignore loading private methods 208 | if "__" in function: 209 | continue 210 | 211 | # Ignore loading commands marked as ignore 212 | if hasattr(_class, "importer_ignore_functions"): 213 | if function in _class.importer_ignore_functions: 214 | continue 215 | 216 | setattr(self.parent.custom_methods, function, getattr(_class, function)) 217 | self.parent.safe_methods.add(function) 218 | 219 | # This initializes methods that are guaranteed safe to use. This mitigates the possibility of clients accessing 220 | # private or sensitive methods. 221 | def init_builtin_cl_methods(self): 222 | for function in dir(self.parent.cl_methods): 223 | # Ignore loading private methods 224 | if "__" in function: 225 | continue 226 | 227 | # Ignore loading commands marked as ignore 228 | if hasattr(self.parent.cl_methods, "importer_ignore_functions"): 229 | if function in self.parent.cl_methods.importer_ignore_functions: 230 | continue 231 | 232 | self.parent.safe_methods.add(function) 233 | 234 | def log(self, event, force: bool = False): 235 | if self.parent.enable_logs or force: 236 | print(f"{self.timestamp()}: {event}") 237 | 238 | def timestamp(self): 239 | today = datetime.now() 240 | return today.strftime("%m/%d/%Y %H:%M.%S") 241 | -------------------------------------------------------------------------------- /python/archived/0.1.8.1/cloudlink/serverRootHandlers.py: -------------------------------------------------------------------------------- 1 | class serverRootHandlers: 2 | """ 3 | The serverRootHandlers inter class is an interface for the WebsocketServer to communicate with Cloudlink. 4 | Cloudlink.serverRootHandlers.onPacket talks to the Cloudlink.packetHandler function to handle packets, 5 | which will call upon Cloudlink.internalHandlers or some other external, custom code (if used). 6 | """ 7 | 8 | def __init__(self, cloudlink): 9 | self.cloudlink = cloudlink 10 | self.supporter = self.cloudlink.supporter 11 | 12 | def on_connect(self, client, server): 13 | if not(client == None): 14 | if self.cloudlink.rejectClientMode: 15 | self.supporter.log(f"Client {client.id} ({client.full_ip}) was rejected.") 16 | 17 | # Reject the client 18 | self.supporter.rejectClient(client, "Connection rejected") 19 | 20 | elif client.friendly_ip in self.cloudlink.ipblocklist: 21 | self.supporter.log(f"Client {client.id} ({client.full_ip}) was blocked from connecting.") 22 | 23 | # Block the client 24 | self.supporter.rejectClient(client, "IP blocked") 25 | else: 26 | self.supporter.log(f"Client {client.id} ({client.full_ip}) connected.") 27 | 28 | # Create attributes for the client 29 | self.supporter.createAttrForClient(client) 30 | 31 | # Join default room 32 | self.supporter.linkClientToRooms(client, "default") 33 | 34 | # Report to the client it's IP address 35 | self.cloudlink.sendPacket(client, {"cmd": "client_ip", "val": str(client.full_ip)}) 36 | 37 | # Report to the client the CL Server version 38 | self.cloudlink.sendPacket(client, {"cmd": "server_version", "val": str(self.cloudlink.version)}) 39 | 40 | # Report to the client the currently cached global message 41 | self.cloudlink.sendPacket(client, {"cmd": "gmsg", "val": self.cloudlink.global_msg}) 42 | 43 | # Update the client's userlist 44 | self.cloudlink.sendPacket(client, {"cmd": "ulist", "val": self.supporter.getUsernames()}) 45 | 46 | # Tell the client the server's Message-Of-The-Day (MOTD) 47 | if self.cloudlink.motd_enable: 48 | self.cloudlink.sendPacket(client, {"cmd": "motd", "val": self.cloudlink.motd_msg}) 49 | 50 | if self.on_connect in self.cloudlink.usercallbacks: 51 | if self.cloudlink.usercallbacks[self.on_connect] != None: 52 | self.cloudlink.usercallbacks[self.on_connect](client=client, server=server) 53 | 54 | def on_close(self, client, server): 55 | if not(client == None): 56 | self.supporter.log(f"Client {client.id} ({client.full_ip}) disconnected.") 57 | 58 | # Remove client from all rooms (Required to prevent BrokenPipeErrors) 59 | self.supporter.removeClientFromAllRooms(client) 60 | 61 | if self.on_close in self.cloudlink.usercallbacks: 62 | if self.cloudlink.usercallbacks[self.on_close] != None: 63 | self.cloudlink.usercallbacks[self.on_close](client=client, server=server) 64 | 65 | def on_packet(self, client, server, message): 66 | if not(client == None): 67 | if len(message) == 0: 68 | self.supporter.log(f"Packet from {client.id} was blank!") 69 | self.cloudlink.sendCode(client, "EmptyPacket") 70 | else: 71 | try: 72 | isPacketSane = self.supporter.isPacketSane(message) 73 | if isPacketSane: 74 | message = self.supporter.json.loads(message) 75 | 76 | # Convert keys in the packet to proper JSON (Primarily for Scratch-based clients) 77 | for key in message.keys(): 78 | if type(message[key]) == str: 79 | if self.supporter.isJSON(message[key]): 80 | message[key] = self.supporter.json.loads(message[key]) 81 | 82 | # Check if the packet contains a listener ID 83 | listener_detected = ("listener" in message) 84 | listener_id = None 85 | if listener_detected: 86 | listener_id = message["listener"] 87 | else: 88 | # Check for old layout (Mainly for Direct command) listeners 89 | if type(message["val"]) == str: 90 | listener_detected = ("listener" in message["val"]) 91 | if listener_detected: 92 | listener_id = message["val"]["listener"] 93 | 94 | room_id = None 95 | if client.is_linked: 96 | room_id = client.rooms 97 | 98 | if "rooms" in message: 99 | if type(message["rooms"]) in [str, list]: 100 | # Convert to set 101 | if type(message["rooms"]) == str: 102 | message["rooms"] = set([message["rooms"]]) 103 | elif type(message["rooms"]) == list: 104 | message["rooms"] = set(message["rooms"]) 105 | room_id = message["rooms"] 106 | 107 | # Remove unlinked rooms 108 | tmp_room_id = room_id.copy() 109 | for room in tmp_room_id: 110 | if not room in client.rooms: 111 | self.supporter.log(f"Client {client.id} ({client.full_ip}) attempted to access room {room}, but was blocked!") 112 | room_id.remove(room) 113 | 114 | # Check if the command is a built-in Cloudlink command 115 | if ((message["cmd"] in self.cloudlink.builtInCommands) and not(message["cmd"] == "direct")): 116 | getattr(self.cloudlink, str(message["cmd"]))(client, server, message, listener_detected, listener_id, room_id) 117 | else: 118 | # Attempt to read the command as a direct or custom command 119 | isCustom = False 120 | isLegacy = False 121 | isValid = True 122 | if message["cmd"] in self.cloudlink.customCommands: 123 | # New custom command system. 124 | isCustom = True 125 | elif message["cmd"] == "direct": 126 | if self.supporter.isPacketSane(message["val"]): 127 | if type(message["val"]) == dict: 128 | if message["val"]["cmd"] in self.cloudlink.customCommands: 129 | # Legacy custom command system (using direct) 130 | isLegacy = True 131 | else: 132 | isCustom = True 133 | else: 134 | isValid = False 135 | if isValid: 136 | if isLegacy: 137 | self.supporter.log(f"Client {client.id} ({client.full_ip}) sent legacy custom command \"{message['val']['cmd']}\"") 138 | getattr(self.cloudlink, str(message["val"]["cmd"]))(client, server, message, listener_detected, listener_id, room_id) 139 | else: 140 | if isCustom: 141 | self.supporter.log(f"Client {client.id} ({client.full_ip}) sent custom command \"{message['cmd']}\"") 142 | getattr(self.cloudlink, str(message["cmd"]))(client, server, message, listener_detected, listener_id, room_id) 143 | else: 144 | getattr(self.cloudlink, "direct")(client, server, message, listener_detected, listener_id, room_id) 145 | else: 146 | if message["cmd"] in self.cloudlink.disabledCommands: 147 | self.supporter.log(f"Client {client.id} ({client.full_ip}) sent custom command \"{message['cmd']}\", but the command is disabled.") 148 | self.cloudlink.sendCode(client, "Disabled") 149 | else: 150 | self.supporter.log(f"Client {client.id} ({client.full_ip}) sent custom command \"{message['cmd']}\", but the command is invalid or it was not loaded.") 151 | self.cloudlink.sendCode(client, "Invalid") 152 | 153 | if self.on_packet in self.cloudlink.usercallbacks: 154 | if self.cloudlink.usercallbacks[self.on_packet] != None: 155 | self.cloudlink.usercallbacks[self.on_packet](client=client, server=server, message=message) 156 | else: 157 | self.supporter.log(f"Packet \"{message}\" is invalid, incomplete, or malformed!") 158 | self.cloudlink.sendCode(client, "Syntax") 159 | except: 160 | self.supporter.log(f"An exception has occurred. {self.supporter.full_stack()}") 161 | self.cloudlink.sendCode(client, "InternalServerError") -------------------------------------------------------------------------------- /python/cloudlink/server/modules/rooms_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | rooms_manager - Provides tools to search, add, and remove rooms from the server. 3 | """ 4 | 5 | 6 | class exceptions: 7 | class RoomDoesNotExist(Exception): 8 | """This exception is raised when a client accesses a room that does not exist""" 9 | pass 10 | 11 | class RoomAlreadyExists(Exception): 12 | """This exception is raised when attempting to create a room that already exists""" 13 | pass 14 | 15 | class RoomNotEmpty(Exception): 16 | """This exception is raised when attempting to delete a room that is not empty""" 17 | pass 18 | 19 | class NoResultsFound(Exception): 20 | """This exception is raised when there are no results for a room search request.""" 21 | pass 22 | 23 | class RoomUnsupportedProtocol(Exception): 24 | """This exception is raised when a room does not support a client's protocol.""" 25 | pass 26 | 27 | 28 | class rooms_manager: 29 | def __init__(self, parent): 30 | # Inherit parent 31 | self.parent = parent 32 | 33 | # Storage of rooms 34 | self.rooms = dict() 35 | 36 | # Init exceptions 37 | self.exceptions = exceptions() 38 | 39 | # Init logger 40 | self.logging = parent.logging 41 | self.logger = self.logging.getLogger(__name__) 42 | 43 | def get(self, room_id): 44 | try: 45 | return self.find_room(room_id) 46 | except self.exceptions.NoResultsFound: 47 | # Return default dict 48 | return { 49 | "clients": dict(), 50 | "global_vars": dict(), 51 | "private_vars": dict() 52 | } 53 | 54 | def create(self, room_id): 55 | # Rooms may only have string names 56 | if type(room_id) != str: 57 | raise TypeError("Room IDs only support strings!") 58 | 59 | # Prevent re-declaring a room 60 | if self.exists(room_id): 61 | raise self.exceptions.RoomAlreadyExists 62 | 63 | # Create the room 64 | self.rooms[room_id] = { 65 | "clients": dict(), 66 | "global_vars": dict(), 67 | "private_vars": dict() 68 | } 69 | 70 | # Log creation 71 | self.parent.logger.debug(f"Created room {room_id}") 72 | 73 | def delete(self, room_id): 74 | # Rooms may only have string names 75 | if type(room_id) != str: 76 | raise TypeError("Room IDs only support strings!") 77 | 78 | # Check if the room exists 79 | if not self.exists(room_id): 80 | raise self.exceptions.RoomDoesNotExist 81 | 82 | # Prevent deleting a room if it's not empty 83 | if len(self.rooms[room_id]["clients"]): 84 | raise self.exceptions.RoomNotEmpty 85 | 86 | # Delete the room 87 | self.rooms.pop(room_id) 88 | 89 | # Log deletion 90 | self.parent.logger.debug(f"Deleted room {room_id}") 91 | 92 | def exists(self, room_id) -> bool: 93 | # Rooms may only have string names 94 | if type(room_id) != str: 95 | raise TypeError("Room IDs only support strings!") 96 | 97 | return room_id in self.rooms 98 | 99 | def subscribe(self, obj, room_id): 100 | # Rooms may only have string names 101 | if type(room_id) != str: 102 | raise TypeError("Room IDs only support strings!") 103 | 104 | # Check if room exists 105 | if not self.exists(room_id): 106 | self.create(room_id) 107 | 108 | room = self.rooms[room_id] 109 | 110 | # Create room protocol categories 111 | if obj.protocol not in room["clients"]: 112 | room["clients"][obj.protocol] = { 113 | "all": set(), 114 | "uuids": dict(), 115 | "snowflakes": dict(), 116 | "usernames": dict() 117 | } 118 | 119 | room_objs = self.rooms[room_id]["clients"][obj.protocol] 120 | 121 | # Exit if client has subscribed to the room already 122 | if str(obj.id) in room_objs["uuids"]: 123 | return 124 | 125 | # Add to room 126 | room_objs["all"].add(obj) 127 | room_objs["uuids"][str(obj.id)] = obj 128 | room_objs["snowflakes"][obj.snowflake] = obj 129 | 130 | # Create room username reference 131 | if obj.username not in room_objs["usernames"]: 132 | room_objs["usernames"][obj.username] = set() 133 | 134 | # Add to usernames reference 135 | room_objs["usernames"][obj.username].add(obj) 136 | 137 | # Add room to client object 138 | obj.rooms.add(room_id) 139 | 140 | # Log room subscribe 141 | self.parent.logger.debug(f"Subscribed client {obj.snowflake} to room {room_id}") 142 | 143 | def unsubscribe(self, obj, room_id): 144 | # Rooms may only have string names 145 | if type(room_id) != str: 146 | raise TypeError("Room IDs only support strings!") 147 | 148 | # Check if room exists 149 | if not self.exists(room_id): 150 | raise self.exceptions.RoomDoesNotExist 151 | 152 | room = self.rooms[room_id]["clients"][obj.protocol] 153 | 154 | # Check if a client has subscribed to a room 155 | if str(obj.id) not in room["uuids"]: 156 | return 157 | 158 | # Remove from room 159 | room["all"].remove(obj) 160 | room["uuids"].pop(str(obj.id)) 161 | room["snowflakes"].pop(obj.snowflake) 162 | if obj in room["usernames"][obj.username]: 163 | room["usernames"][obj.username].remove(obj) 164 | 165 | # Remove empty username reference set 166 | if not len(room["usernames"][obj.username]): 167 | room["usernames"].pop(obj.username) 168 | 169 | room = self.rooms[room_id]["clients"] 170 | 171 | # Clean up room protocol categories 172 | if not len(room[obj.protocol]["all"]): 173 | room.pop(obj.protocol) 174 | 175 | # Remove room from client object 176 | obj.rooms.remove(room_id) 177 | 178 | # Log room unsubscribe 179 | self.parent.logger.debug(f"Unsubscribed client {obj.snowflake} from room {room_id}") 180 | 181 | # Delete empty room 182 | if not len(room): 183 | self.parent.logger.debug(f"Deleting emptied room {room_id}...") 184 | self.delete(room_id) 185 | 186 | def find_room(self, query): 187 | # Rooms may only have string names 188 | if type(query) != str: 189 | raise TypeError("Searching for room objects requires a string for the query.") 190 | if query in (room for room in self.rooms): 191 | return self.rooms[query] 192 | else: 193 | raise self.exceptions.NoResultsFound 194 | 195 | def find_obj(self, query, room): 196 | # Prevent accessing clients with usernames not being set 197 | if not len(query): 198 | raise self.exceptions.NoResultsFound 199 | 200 | # Locate client objects in room 201 | if query in room["usernames"]: 202 | return room["usernames"][query] # returns set of client objects 203 | elif query in self.get_uuids(room): 204 | return room["uuids"][query] # returns client object 205 | elif query in self.get_snowflakes(room): 206 | return room["snowflakes"][query] # returns client object 207 | else: 208 | raise self.exceptions.NoResultsFound 209 | 210 | def generate_userlist(self, room_id, protocol) -> list: 211 | userlist = list() 212 | 213 | room = self.get(room_id)["clients"][protocol]["all"] 214 | 215 | for obj in room: 216 | if not obj.username_set: 217 | continue 218 | 219 | userlist.append({ 220 | "id": obj.snowflake, 221 | "username": obj.username, 222 | "uuid": str(obj.id) 223 | }) 224 | 225 | return userlist 226 | 227 | def get_snowflakes(self, room) -> set: 228 | return set(obj for obj in room["snowflakes"]) 229 | 230 | def get_uuids(self, room) -> set: 231 | return set(obj for obj in room["uuids"]) 232 | 233 | async def get_all_in_rooms(self, rooms, protocol) -> set: 234 | obj_set = set() 235 | 236 | # Validate types 237 | if type(rooms) not in [list, set, str]: 238 | raise TypeError(f"Gathering all user objects in rooms requires using a list, set, or string! Got {type(rooms)}.") 239 | 240 | # Convert to set 241 | if type(rooms) == str: 242 | rooms = {rooms} 243 | if type(rooms) == list: 244 | rooms = set(rooms) 245 | 246 | # Collect all user objects in rooms 247 | async for room in self.parent.async_iterable(rooms): 248 | if protocol not in self.get(room)["clients"]: 249 | continue 250 | obj_set.update(self.get(room)["clients"][protocol]["all"]) 251 | 252 | return obj_set 253 | 254 | async def get_specific_in_room(self, room, protocol, queries) -> set: 255 | obj_set = set() 256 | 257 | # Validate types 258 | if type(room) != str: 259 | raise TypeError(f"Gathering specific clients in a room only supports strings for room IDs.") 260 | if type(queries) not in [list, set, str]: 261 | raise TypeError(f"Gathering all user objects in a room requires using a list, set, or string! Got {type(queries)}.") 262 | 263 | # Just return an empty set if the room doesn't exist 264 | if not self.exists(room): 265 | return set() 266 | 267 | room = self.get(room)["clients"][protocol] 268 | 269 | # Convert queries to set 270 | if type(queries) == str: 271 | queries = {queries} 272 | if type(queries) == list: 273 | queries = set(queries) 274 | 275 | async for query in self.parent.async_iterable(queries): 276 | try: 277 | obj = self.find_obj(query, room) 278 | if type(obj) == set: 279 | obj_set.update(obj) 280 | else: 281 | obj_set.add(obj) 282 | except self.exceptions.NoResultsFound: 283 | continue 284 | 285 | return obj_set 286 | --------------------------------------------------------------------------------