Initial commit
This commit is contained in:
201
.gitignore
vendored
Normal file
201
.gitignore
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
20
license.md
Normal file
20
license.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## ZLib
|
||||
```
|
||||
Copyright (c) 2024 Kyler "Félix" Eastridge
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
```
|
||||
0
pymetaverse/__init__.py
Normal file
0
pymetaverse/__init__.py
Normal file
45
pymetaverse/agent.py
Normal file
45
pymetaverse/agent.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from .simulator import Simulator
|
||||
from .eventtarget import EventTarget
|
||||
from . import messages
|
||||
import asyncio
|
||||
|
||||
class Agent:
|
||||
def __init__(self):
|
||||
self.simulator = None
|
||||
self.simulators = []
|
||||
self.messageTemplate = messages.getDefaultTemplate()
|
||||
|
||||
async def addSimulator(self, host, circuit, caps = None, parent = False):
|
||||
sim = Simulator(self)
|
||||
await sim.connect(host, circuit)
|
||||
self.simulators.append(sim)
|
||||
|
||||
if caps:
|
||||
await sim.fetchCapabilities(caps)
|
||||
|
||||
if parent:
|
||||
self.simulator = sim
|
||||
|
||||
return sim
|
||||
|
||||
@classmethod
|
||||
async def fromLogin(cls, login):
|
||||
self = cls()
|
||||
self.agentId = login["agent_id"]
|
||||
self.sessionId = login["session_id"]
|
||||
self.secureSessionId = login["secure_session_id"]
|
||||
await self.addSimulator((login["sim_ip"], login["sim_port"]), login["circuit_code"], login["seed_capability"], True)
|
||||
msg = self.messageTemplate.getMessage("CompleteAgentMovement")
|
||||
msg.AgentData.AgentID = self.agentId
|
||||
msg.AgentData.SessionID = self.sessionId
|
||||
msg.AgentData.CircuitCode = login["circuit_code"]
|
||||
self.simulator.send(msg, True)
|
||||
return self
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
if self.simulator == None:
|
||||
print("Simulator gone! :(")
|
||||
break
|
||||
|
||||
64
pymetaverse/circuit.py
Normal file
64
pymetaverse/circuit.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
from .eventtarget import EventTarget
|
||||
from . import packet
|
||||
|
||||
class Circuit(asyncio.Protocol, EventTarget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transport = None
|
||||
self.sequence = 0
|
||||
self.unackd = {}
|
||||
self.acks = []
|
||||
|
||||
def nextSequence(self):
|
||||
seq = self.sequence
|
||||
self.sequence += 1
|
||||
return seq
|
||||
|
||||
def acknowledge(self, sequences):
|
||||
for ack in sequences:
|
||||
if ack in self.unackd:
|
||||
self.unackd.pop(ack)
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
def datagram_received(self, data, addr):
|
||||
pkt = packet.Packet.fromBytes(data)
|
||||
if pkt.reliable:
|
||||
self.acks.append(pkt.sequence)
|
||||
|
||||
# Has acks, acknowledge them!
|
||||
if pkt.flags & pkt.FLAGS.ACK:
|
||||
self.acknowledge(pkt.acks)
|
||||
|
||||
self.fire("message", addr, pkt.body)
|
||||
|
||||
def error_received(self, exc):
|
||||
self.fire("error", exc)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.fire("close", exc)
|
||||
|
||||
def send(self, message, reliable = False):
|
||||
pkt = packet.Packet(self.nextSequence(), bytes(message), acks=self.acks)
|
||||
if reliable:
|
||||
pkt.reliable = True
|
||||
self.unackd[pkt.sequence] = pkt
|
||||
|
||||
self.transport.sendto(pkt.toBytes())
|
||||
|
||||
def resend(self, distance = 100):
|
||||
cutoff = self.sequence - distance
|
||||
for pkt in self.unackd.values():
|
||||
if pkt.sequence < cutoff:
|
||||
pkt.resent = True
|
||||
self.transport.sendto(pkt.toBytes())
|
||||
|
||||
@classmethod
|
||||
async def create(cls, host, loop = None):
|
||||
loop = loop or asyncio.get_running_loop()
|
||||
transport, protocol = await loop.create_datagram_endpoint(
|
||||
lambda: cls(),
|
||||
remote_addr=host)
|
||||
return protocol
|
||||
24
pymetaverse/eventtarget.py
Normal file
24
pymetaverse/eventtarget.py
Normal file
@@ -0,0 +1,24 @@
|
||||
class EventTarget:
|
||||
def __init__(self):
|
||||
self._listeners = {}
|
||||
|
||||
def on(self, event, func = None):
|
||||
if not event in self._listeners:
|
||||
self._listeners[event] = []
|
||||
|
||||
def _(func):
|
||||
self._listeners[event].append(func)
|
||||
return func
|
||||
|
||||
if func:
|
||||
return _(func)
|
||||
return _
|
||||
|
||||
def off(self, event, func):
|
||||
if event in self._listeners:
|
||||
self._listeners[event].remove(func)
|
||||
|
||||
def fire(self, event, *args, **kwargs):
|
||||
if event in self._listeners:
|
||||
for func in self._listeners[event]:
|
||||
func(*args, **kwargs)
|
||||
61
pymetaverse/httpclient.py
Normal file
61
pymetaverse/httpclient.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import aiohttp
|
||||
|
||||
class HttpResponse:
|
||||
def __init__(self, handle):
|
||||
self._handle = handle
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
self._handle.close()
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._handle.status
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._handle.headers
|
||||
|
||||
async def read(self):
|
||||
return await self._handle.read()
|
||||
|
||||
class HttpClient:
|
||||
def __init__(self):
|
||||
self._session = None
|
||||
|
||||
async def __aenter__(self):
|
||||
self._session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self._session.close()
|
||||
|
||||
async def get(self, url, **kwargs):
|
||||
response = await self._session.get(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
|
||||
async def post(self, url, **kwargs):
|
||||
response = await self._session.post(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
|
||||
async def put(self, url, **kwargs):
|
||||
response = await self._session.put(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
|
||||
async def delete(self, url, **kwargs):
|
||||
response = await self._session.delete(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
|
||||
async def head(self, url, **kwargs):
|
||||
response = await self._session.head(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
|
||||
async def options(self, url, **kwargs):
|
||||
response = await self._session.options(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
|
||||
async def patch(self, url, **kwargs):
|
||||
response = await self._session.patch(url, **kwargs)
|
||||
return HttpResponse(response)
|
||||
380
pymetaverse/llsd.py
Normal file
380
pymetaverse/llsd.py
Normal file
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Name: llsd.py
|
||||
Purpose: Parse and serialize LLSD objects
|
||||
|
||||
Copyright (c) 2021 Kyler Eastridge
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import datetime
|
||||
import base64
|
||||
import io
|
||||
import struct
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class URI(str):
|
||||
def __repr__(self):
|
||||
return "URI({})".format(super().__repr__())
|
||||
|
||||
#Encoders
|
||||
def llsdEncodeXml(input, destination, *args, optimize = False, encoding = "base64", **kwargs):
|
||||
t = type(input)
|
||||
if input == None:
|
||||
elm = ET.SubElement(destination, "undef")
|
||||
elif t == bool:
|
||||
elm = ET.SubElement(destination, "boolean")
|
||||
if input:
|
||||
elm.text = "true"
|
||||
elif not optimize:
|
||||
elm.text = "false"
|
||||
elif t == int:
|
||||
elm = ET.SubElement(destination, "integer")
|
||||
if input != 0 or not optimize:
|
||||
elm.text = str(input)
|
||||
elif t == float:
|
||||
elm = ET.SubElement(destination, "real")
|
||||
if input != 0 or not optimize:
|
||||
elm.text = str(input)
|
||||
elif t == uuid.UUID:
|
||||
elm = ET.SubElement(destination, "uuid")
|
||||
if input.bytes != b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" or not optimize:
|
||||
elm.text = str(input)
|
||||
elif t == str:
|
||||
elm = ET.SubElement(destination, "string")
|
||||
if input != "" or not optimize:
|
||||
elm.text = input
|
||||
elif t == bytes:
|
||||
encoder = encoding
|
||||
elm = ET.SubElement(destination, "binary")
|
||||
if input != b"" or not optimize:
|
||||
if encoder == "base64":
|
||||
elm.text = base64.b64encode(input).decode()
|
||||
elif encoder == "base85":
|
||||
elm.text = base64.b85encode(input).decode()
|
||||
elif encoder == "base16":
|
||||
elm.text = base64.b16encode(input).decode()
|
||||
else:
|
||||
raise ValueError("Unknown binary encoding {}!".format(encoder))
|
||||
elif t == datetime.datetime:
|
||||
elm = ET.SubElement(destination, "date")
|
||||
elm.text = input.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
elif t == URI:
|
||||
elm = ET.SubElement(destination, "uri")
|
||||
if input != "" or not optimize:
|
||||
elm.text = input
|
||||
elif t == dict:
|
||||
root = ET.SubElement(destination, "map")
|
||||
for key in input:
|
||||
if type(key) != str:
|
||||
raise ValueError("Dictionary keys must be type str, not {}!".format(type(key)))
|
||||
elm = ET.SubElement(root, "key")
|
||||
elm.text = key
|
||||
llsdEncodeXml(input[key], root, *args, **kwargs)
|
||||
elif t == list:
|
||||
root = ET.SubElement(destination, "array")
|
||||
for value in input:
|
||||
llsdEncodeXml(value, root, *args, **kwargs)
|
||||
|
||||
def llsdEncode(input, *args, format = "xml", **kwargs):
|
||||
if format == "xml":
|
||||
root = ET.Element("llsd")
|
||||
if "optimize" not in kwargs:
|
||||
kwargs["optimize"] = True
|
||||
llsdEncodeXml(input, root, *args, **kwargs)
|
||||
xml = ET.ElementTree(root)
|
||||
f = io.BytesIO()
|
||||
xml.write(f, encoding='UTF-8', xml_declaration=True)
|
||||
return f.getvalue()
|
||||
|
||||
#Decoders
|
||||
def parseISODate(input):
|
||||
try:
|
||||
if input[-1] == "Z":
|
||||
input = input[:-1]
|
||||
date, time = input.split("T", 2)
|
||||
year, month, day = date.split("-", 3)
|
||||
hour, minute, second = time.split(":", 3)
|
||||
if "." in second:
|
||||
second, microsecond = second.split(".", 2)
|
||||
else:
|
||||
microsecond = 0
|
||||
return datetime.datetime(*[int(i) for i in [year, month, day, hour, minute, second, microsecond]])
|
||||
except ValueError:
|
||||
raise ValueError("Invalid timestamp '{}'!".format(input))
|
||||
|
||||
def llsdDecodeXml(input):
|
||||
if input.tag == "undef":
|
||||
return None
|
||||
elif input.tag == "boolean":
|
||||
value = input.text
|
||||
if value == None:
|
||||
return False
|
||||
value = value.lower()
|
||||
if value in ["1", "true"]:
|
||||
return True
|
||||
elif value in ["", "0", "false"]:
|
||||
return False
|
||||
else:
|
||||
raise ValueError("Unexpected value '{}' for boolean!".format(value))
|
||||
elif input.tag == "integer":
|
||||
if input.text == None:
|
||||
return 0
|
||||
return int(input.text)
|
||||
elif input.tag == "real":
|
||||
if input.text == None:
|
||||
return 0
|
||||
return float(input.text)
|
||||
elif input.tag == "uuid":
|
||||
if input.text == None:
|
||||
return uuid.UUID(bytes=b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")
|
||||
return uuid.UUID(input.text)
|
||||
elif input.tag == "string":
|
||||
if input.text == None:
|
||||
return ""
|
||||
return input.text
|
||||
elif input.tag == "binary":
|
||||
if input.text == None:
|
||||
return b""
|
||||
encoding = input.attrib.get("encoding", "base64").lower()
|
||||
if encoding == "base64":
|
||||
return base64.b64decode(input.text)
|
||||
elif encoding == "base85":
|
||||
return base64.b85decode(input.text)
|
||||
elif encoding == "base16":
|
||||
return base64.b16decode(input.text)
|
||||
else:
|
||||
raise ValueError("Unknown encoding {} for binary element!".format(encoding))
|
||||
elif input.tag == "date":
|
||||
if input.text == None:
|
||||
return datetime.datetime.fromtimestamp(0)
|
||||
return parseISODate(input.text)
|
||||
elif input.tag == "uri":
|
||||
if input.text == None:
|
||||
return URI("")
|
||||
return URI(input.text)
|
||||
elif input.tag == "map":
|
||||
result = {}
|
||||
for i in range(0, len(input), 2):
|
||||
if input[i].tag != "key":
|
||||
raise ValueError("Unexpected {} element in map, expected key!".format(input[i].tag))
|
||||
result[input[i].text] = llsdDecodeXml(input[i+1])
|
||||
return result
|
||||
elif input.tag == "array":
|
||||
result = [None]*len(input)
|
||||
for i in range(0, len(input)):
|
||||
result[i] = llsdDecodeXml(input[i])
|
||||
return result
|
||||
else:
|
||||
raise ValueError("Unexpected {} element in LLSD!".format(input.tag))
|
||||
|
||||
def llsdDecode(input, *args, format = None, maxHeaderLength = 128, **kwargs):
|
||||
if format == None:
|
||||
isBytes = type(input) == bytes
|
||||
i = 0
|
||||
l = len(input)
|
||||
while i < l and i < maxHeaderLength:
|
||||
if isBytes:
|
||||
c = chr(input[i])
|
||||
else:
|
||||
c = input[i]
|
||||
if c == '"' or c == "'":
|
||||
quoteChar = c
|
||||
while i < l and i < maxHeaderLength:
|
||||
i += 1
|
||||
if isBytes:
|
||||
c = chr(input[i])
|
||||
else:
|
||||
c = input[i]
|
||||
if c == quoteChar:
|
||||
break
|
||||
elif c == "\\":
|
||||
#Assuming the file is valid, no unicode should be in the
|
||||
#header
|
||||
i += 1
|
||||
i += 1
|
||||
i += 1
|
||||
if c == ">":
|
||||
break
|
||||
header = input[2:i-2].strip().lower()
|
||||
if header == "llsd/notation":
|
||||
format = "notation"
|
||||
elif header == "llsd/binary":
|
||||
format = "binary"
|
||||
else:
|
||||
tmp = header[0:3]
|
||||
if type(tmp) == bytes:
|
||||
tmp = tmp.decode()
|
||||
if tmp == "xml":
|
||||
format = "xml"
|
||||
else:
|
||||
raise ValueError("Unable to detect serialization format!")
|
||||
|
||||
if format == "xml":
|
||||
input = ET.fromstring(input)
|
||||
if input.tag != "llsd":
|
||||
raise ValueError("Unexpected tag {} in LLSD+XML!".format(input.tag))
|
||||
return llsdDecodeXml(input[0])
|
||||
else:
|
||||
raise ValueError("Unknown serialization format {}!".format(format))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
source_test = {
|
||||
"undef": [None],
|
||||
"boolean": [True, False],
|
||||
"integer": [289343, -3, 0],
|
||||
"real": [-0.28334, 2983287453.3848387, 0.0],
|
||||
"uuid": [
|
||||
uuid.UUID("d7f4aeca-88f1-42a1-b385-b9db18abb255"),
|
||||
uuid.UUID("00000000-0000-0000-0000-000000000000")
|
||||
],
|
||||
"string": [
|
||||
"The quick brown fox jumped over the lazy dog.",
|
||||
"540943c1-7142-4fdd-996f-fc90ed5dd3fa",
|
||||
""
|
||||
],
|
||||
"binary": [
|
||||
b"The quick brown fox jumped over the lazy dog."
|
||||
],
|
||||
"date": [
|
||||
datetime.datetime.now()
|
||||
],
|
||||
"uri": [
|
||||
URI("http://sim956.agni.lindenlab.com:12035/runtime/agents")
|
||||
]
|
||||
}
|
||||
llsd_xml_test = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<llsd>
|
||||
<map>
|
||||
<key>undef</key>
|
||||
<array>
|
||||
<undef />
|
||||
</array>
|
||||
<key>boolean</key>
|
||||
<array>
|
||||
<!-- true -->
|
||||
<boolean>1</boolean>
|
||||
<boolean>true</boolean>
|
||||
|
||||
<!-- false -->
|
||||
<boolean>0</boolean>
|
||||
<boolean>false</boolean>
|
||||
<boolean />
|
||||
</array>
|
||||
<key>integer</key>
|
||||
<array>
|
||||
<integer>289343</integer>
|
||||
<integer>-3</integer>
|
||||
<integer /> <!-- zero -->
|
||||
</array>
|
||||
<key>real</key>
|
||||
<array>
|
||||
<real>-0.28334</real>
|
||||
<real>2983287453.3848387</real>
|
||||
<real /> <!-- exactly zero -->
|
||||
</array>
|
||||
<key>uuid</key>
|
||||
<array>
|
||||
<uuid>d7f4aeca-88f1-42a1-b385-b9db18abb255</uuid>
|
||||
<uuid /> <!-- null uuid '00000000-0000-0000-0000-000000000000' -->
|
||||
</array>
|
||||
<key>string</key>
|
||||
<array>
|
||||
<string>The quick brown fox jumped over the lazy dog.</string>
|
||||
<string>540943c1-7142-4fdd-996f-fc90ed5dd3fa</string>
|
||||
<string /> <!-- empty string -->
|
||||
</array>
|
||||
<key>binary</key>
|
||||
<array>
|
||||
<binary encoding="base64">cmFuZG9t</binary> <!-- base 64 encoded binary data -->
|
||||
<binary>dGhlIHF1aWNrIGJyb3duIGZveA==</binary> <!-- base 64 encoded binary data is default -->
|
||||
<binary encoding="base85">YISXJWn>_4c4cxPbZBJ</binary>
|
||||
<binary encoding="base16">6C617A7920646F67</binary>
|
||||
<binary /> <!-- empty binary blob -->
|
||||
</array>
|
||||
<key>date</key>
|
||||
<array>
|
||||
<date>2006-02-01T14:29:53.43Z</date>
|
||||
<date /> <!-- epoch -->
|
||||
</array>
|
||||
<key>uri</key>
|
||||
<array>
|
||||
<uri>http://sim956.agni.lindenlab.com:12035/runtime/agents</uri>
|
||||
<uri /> <!-- an empty link -->
|
||||
</array>
|
||||
</map>
|
||||
</llsd>"""
|
||||
|
||||
llsd_notation_test = """<?llsd/notation?>
|
||||
{
|
||||
'undef':
|
||||
[
|
||||
!
|
||||
],
|
||||
'boolean':
|
||||
[
|
||||
1,
|
||||
t,
|
||||
T,
|
||||
true,
|
||||
TRUE,
|
||||
0
|
||||
f,
|
||||
F,
|
||||
false,
|
||||
FALSE
|
||||
],
|
||||
'integer':
|
||||
[
|
||||
i289343,
|
||||
i-3
|
||||
],
|
||||
'real':
|
||||
[
|
||||
r-0.28334,
|
||||
r2983287453.3848387
|
||||
]
|
||||
'uuid':
|
||||
[
|
||||
ud7f4aeca-88f1-42a1-b385-b9db18abb255
|
||||
]
|
||||
'string';
|
||||
'The quick brown fox jumped over the lazy dog.'
|
||||
"540943c1-7142-4fdd-996f-fc90ed5dd3fa",
|
||||
s(10)'0123456789',
|
||||
s(10)"0123456789"
|
||||
]
|
||||
'binary':
|
||||
[
|
||||
b64"cmFuZG9t",
|
||||
b64'dGhlIHF1aWNrIGJyb3duIGZveA==',
|
||||
b85"YISXJWn>_4c4cxPbZBJ",
|
||||
b16'6C617A7920646F67',
|
||||
b(10)'0123456789'
|
||||
]
|
||||
'date':
|
||||
[
|
||||
d"2006-02-01T14:29:53.43Z"
|
||||
]
|
||||
'uri':
|
||||
[
|
||||
l"http://sim956.agni.lindenlab.com:12035/runtime/agents"
|
||||
]
|
||||
}"""
|
||||
168
pymetaverse/login.py
Executable file
168
pymetaverse/login.py
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import hashlib
|
||||
import uuid #For Mac addresses
|
||||
from . import httpclient
|
||||
from . import llsd
|
||||
|
||||
def getMacAddress():
|
||||
mac = uuid.getnode()
|
||||
return ':'.join(("%012X" % mac)[i:i+2] for i in range(0, 12, 2))
|
||||
|
||||
def getPlatform():
|
||||
if sys.platform == "linux" or sys.platform == "linux2":
|
||||
return "Lnx"
|
||||
|
||||
elif sys.platform == "darwin":
|
||||
return "Mac"
|
||||
|
||||
elif sys.platform == "win32":
|
||||
return "Win"
|
||||
|
||||
return "Unk"
|
||||
|
||||
#Convenience name
|
||||
OPTIONS_NONE = []
|
||||
|
||||
#Enough to get us most capability out of the grid without restrictions
|
||||
OPTIONS_MINIMAL = [
|
||||
"adult_compliant",
|
||||
]
|
||||
|
||||
#Normal stuff
|
||||
OPTIONS_MOST = [
|
||||
"inventory-root",
|
||||
|
||||
"inventory-lib-root",
|
||||
"inventory-lib-owner",
|
||||
|
||||
"display_names",
|
||||
"adult_compliant",
|
||||
|
||||
"advanced-mode",
|
||||
|
||||
"max_groups",
|
||||
"max-agent-groups",
|
||||
"map-server-url",
|
||||
"login-flags",
|
||||
]
|
||||
|
||||
#This may take longer to log in
|
||||
OPTIONS_FULL = [
|
||||
"inventory-root",
|
||||
"inventory-skeleton",
|
||||
"inventory-meat",
|
||||
"inventory-skel-targets",
|
||||
|
||||
"inventory-lib-root",
|
||||
"inventory-lib-owner",
|
||||
"inventory-skel-lib",
|
||||
"inventory-meat-lib",
|
||||
|
||||
"initial-outfit",
|
||||
"gestures",
|
||||
"display_names",
|
||||
"event_categories",
|
||||
"event_notifications",
|
||||
"classified_categories",
|
||||
"adult_compliant",
|
||||
"buddy-list",
|
||||
"newuser-config",
|
||||
"ui-config",
|
||||
|
||||
"advanced-mode",
|
||||
|
||||
"max_groups",
|
||||
"max-agent-groups",
|
||||
"map-server-url",
|
||||
"voice-config",
|
||||
"tutorial_setting",
|
||||
"login-flags",
|
||||
"global-textures",
|
||||
#"god-connect", #lol no
|
||||
]
|
||||
|
||||
#WARNING: Not actually async yet! But make it a async request to allow it to be
|
||||
#done so in the future!
|
||||
async def Login(username, password,
|
||||
start = "last",
|
||||
options = None,
|
||||
grid = "https://login.agni.lindenlab.com/cgi-bin/login.cgi",
|
||||
isBot = True
|
||||
):
|
||||
|
||||
if len(username) == 1:
|
||||
username = (username[0], "resident")
|
||||
|
||||
#WARNING:
|
||||
# Falsifying this is a violation of the Terms of Service
|
||||
mac = getMacAddress()
|
||||
|
||||
platform = getPlatform()
|
||||
|
||||
#WARNING:
|
||||
# Deviating from this format MAY be a violation of the Terms of Service
|
||||
id0 = hashlib.md5("{}:{}:{}".format(
|
||||
platform,
|
||||
mac,
|
||||
sys.version
|
||||
).encode("latin")
|
||||
).hexdigest()
|
||||
|
||||
if options == None:
|
||||
options = OPTIONS_MOST
|
||||
|
||||
requestBody = llsd.llsdEncode({
|
||||
#Credentials
|
||||
"first": username[0],
|
||||
"last": username[1],
|
||||
"passwd": "$1$" + hashlib.md5(password.encode("latin")).hexdigest(),
|
||||
#"web_login_key": "",
|
||||
|
||||
#OS information
|
||||
"platform": platform,
|
||||
"platform_version": sys.version,
|
||||
|
||||
#Viewer information
|
||||
"channel": "pymetaverse",
|
||||
"version": "Testing", #TODO: Change this to metaverse.__VERSION__
|
||||
#"major": 0,
|
||||
#"minor": 0,
|
||||
#"patch": 0,
|
||||
#"build": 0,
|
||||
|
||||
#Machine information
|
||||
"mac": mac, #WARNING: Falsifying this is a violation of the Terms of Service
|
||||
"id0": id0, #WARNING: Falsifying this is a violation of the Terms of Service
|
||||
#"viewer_digest": "",
|
||||
|
||||
#Ignore messages for now
|
||||
"skipoptional": True,
|
||||
"agree_to_tos": True,
|
||||
"read_critical": True,
|
||||
|
||||
#Viewer options
|
||||
"extended_errors": True,
|
||||
"options": options,
|
||||
"agent_flags": 2 if isBot else 0, #Bitmask, we are a bot, so set bit 2 to true,
|
||||
"start": start,
|
||||
#"functions": "", #No idea what this does
|
||||
|
||||
#Login error tracking
|
||||
"last_exec_event": 0,
|
||||
#"last_exec_froze": False,
|
||||
#"last_exec_duration": 0,
|
||||
|
||||
#For proxied connections apparently:
|
||||
#"service_proxy_ip": "",
|
||||
|
||||
"token": "",
|
||||
"mfa_hash": ""
|
||||
})
|
||||
|
||||
async with httpclient.HttpClient() as session:
|
||||
async with await session.post(grid, data = requestBody, headers = {
|
||||
"Content-Type": "application/llsd+xml"
|
||||
}) as response:
|
||||
return llsd.llsdDecode(await response.read())
|
||||
|
||||
9219
pymetaverse/message_template.msg
Executable file
9219
pymetaverse/message_template.msg
Executable file
File diff suppressed because it is too large
Load Diff
1
pymetaverse/message_template.msg.sha1
Executable file
1
pymetaverse/message_template.msg.sha1
Executable file
@@ -0,0 +1 @@
|
||||
1a9a3717fde5d0fb3d5f688a1a3dab7fcc2aa308
|
||||
732
pymetaverse/messages.py
Normal file
732
pymetaverse/messages.py
Normal file
@@ -0,0 +1,732 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections import OrderedDict
|
||||
from enum import Enum, auto
|
||||
import ipaddress
|
||||
import uuid
|
||||
import struct
|
||||
import io
|
||||
import os
|
||||
|
||||
# These are shared in various places around the code
|
||||
sUInt32 = struct.Struct(">I")
|
||||
sUInt16 = struct.Struct(">H")
|
||||
sUInt8 = struct.Struct(">B")
|
||||
|
||||
def ZeroEncode(buf):
|
||||
output = io.BytesIO()
|
||||
count = None
|
||||
i = 0
|
||||
l = len(buf)
|
||||
count = 0
|
||||
while i < l:
|
||||
if buf[i] == 0:
|
||||
count = 0
|
||||
while i < l and buf[i] == 0:
|
||||
count += 1
|
||||
i += 1
|
||||
if count == 255:
|
||||
output.write(bytes([0, 0xFF]))
|
||||
count = 0
|
||||
if count != 0:
|
||||
output.write(bytes([0, count]))
|
||||
if i < l:
|
||||
output.write(bytes([buf[i]]))
|
||||
i += 1
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
def ZeroDecode(buf):
|
||||
output = io.BytesIO()
|
||||
count = None
|
||||
i = 0
|
||||
l = len(buf)
|
||||
while i < l:
|
||||
if buf[i] == 0:
|
||||
i += 1
|
||||
output.write(bytes(buf[i]))
|
||||
else:
|
||||
output.write(bytes([buf[i]]))
|
||||
i += 1
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
class Block:
|
||||
class TYPE(Enum):
|
||||
NULL = auto()
|
||||
FIXED = auto()
|
||||
VARIABLE = auto()
|
||||
U8 = auto()
|
||||
U16 = auto()
|
||||
U32 = auto()
|
||||
U64 = auto()
|
||||
S8 = auto()
|
||||
S16 = auto()
|
||||
S32 = auto()
|
||||
S64 = auto()
|
||||
F32 = auto()
|
||||
F64 = auto()
|
||||
LLVECTOR3 = auto()
|
||||
LLVECTOR3D = auto()
|
||||
LLVECTOR4 = auto()
|
||||
LLQUATERNION = auto()
|
||||
LLUUID = auto()
|
||||
BOOL = auto()
|
||||
IPADDR = auto()
|
||||
IPPORT = auto()
|
||||
|
||||
#Unused
|
||||
U16VEC3 = auto()
|
||||
U16QUAT = auto()
|
||||
S16ARRAY = auto()
|
||||
|
||||
# LL couldn't decide which endianness to use. The different endianness
|
||||
# is intentional.
|
||||
sVariable1 = struct.Struct(">B")
|
||||
sVariable2 = struct.Struct(">H")
|
||||
sU8 = struct.Struct("<B")
|
||||
sU16 = struct.Struct("<H")
|
||||
sU32 = struct.Struct("<I")
|
||||
sU64 = struct.Struct("<Q")
|
||||
sS8 = struct.Struct("<b")
|
||||
sS16 = struct.Struct("<h")
|
||||
sS32 = struct.Struct("<i")
|
||||
sS64 = struct.Struct("<q")
|
||||
sF32 = struct.Struct("<f")
|
||||
sF64 = struct.Struct("<d")
|
||||
sLLVector3 = struct.Struct("<fff")
|
||||
sLLVector3d = struct.Struct("<ddd")
|
||||
sLLVector4 = struct.Struct("<ffff")
|
||||
|
||||
def __init__(self, name):
|
||||
super().__setattr__('name', name)
|
||||
super().__setattr__('parameters', OrderedDict())
|
||||
super().__setattr__('values', {})
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.name}>"
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.parameters:
|
||||
return self.values.get(name) # Fix incorrect variable name
|
||||
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in self.parameters:
|
||||
self.values[name] = value # Fix incorrect variable name
|
||||
else:
|
||||
super().__setattr__(name, value) # Prevent infinite recursion
|
||||
|
||||
def __bytes__(self):
|
||||
res = io.BytesIO()
|
||||
self.toStream(res)
|
||||
return res.getvalue()
|
||||
|
||||
def toStream(self, handle):
|
||||
for name, (type, size) in self.parameters.items():
|
||||
if type == self.TYPE.NULL:
|
||||
pass
|
||||
|
||||
elif type == self.TYPE.FIXED:
|
||||
data = self.values.get(name, b"")[:size]
|
||||
handle.write(data.ljust(size, b'\x00'))
|
||||
|
||||
elif type == self.TYPE.VARIABLE:
|
||||
if size == 1:
|
||||
data = self.values.get(name, b"")[:255]
|
||||
handle.write(self.sVariable1.pack(len(data)))
|
||||
handle.write(data)
|
||||
|
||||
elif size == 2:
|
||||
data = self.values.get(name, b"")[:65535]
|
||||
handle.write(self.sVariable2.pack(len(data)))
|
||||
handle.write(data)
|
||||
|
||||
else:
|
||||
raise Exception("Invalid variable size {}".format(size))
|
||||
|
||||
elif type == self.TYPE.U8:
|
||||
handle.write(self.sU8.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.U16:
|
||||
handle.write(self.sU16.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.U32:
|
||||
handle.write(self.sU32.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.U64:
|
||||
handle.write(self.sU64.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.S8:
|
||||
handle.write(self.sS8.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.S16:
|
||||
handle.write(self.sS16.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.S32:
|
||||
handle.write(self.sS32.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.S64:
|
||||
handle.write(self.sS64.pack(int(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.F32:
|
||||
handle.write(self.sF32.pack(float(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.F64:
|
||||
handle.write(self.sF64.pack(float(self.values.get(name, 0) or 0)))
|
||||
|
||||
elif type == self.TYPE.LLVECTOR3:
|
||||
vec = self.values.get(name, (0,0,0)) or (0,0,0)
|
||||
handle.write(self.sLLVector3.pack(vec[0], vec[1], vec[2]))
|
||||
|
||||
elif type == self.TYPE.LLVECTOR3D:
|
||||
vec = self.values.get(name, (0,0,0)) or (0,0,0)
|
||||
handle.write(self.sLLVector3d.pack(vec[0], vec[1], vec[2]))
|
||||
|
||||
elif type == self.TYPE.LLVECTOR4:
|
||||
vec = self.values.get(name, (0,0,0,0)) or (0,0,0,0)
|
||||
handle.write(self.sLLVector4.pack(vec[0], vec[1], vec[2], vec[3]))
|
||||
|
||||
elif type == self.TYPE.LLQUATERNION:
|
||||
vec = self.values.get(name, (0,0,0)) or (0,0,0)
|
||||
# NOTE: Quaternions are transmitted as vectors. The W component
|
||||
# is missing and is just generated on the fly.
|
||||
handle.write(self.sLLVector3.pack(vec[0], vec[1], vec[2]))
|
||||
|
||||
elif type == self.TYPE.LLUUID:
|
||||
handle.write(uuid.UUID(self.values.get(name, "00000000-0000-0000-0000-000000000000") or "00000000-0000-0000-0000-000000000000").bytes)
|
||||
|
||||
elif type == self.TYPE.BOOL:
|
||||
handle.write(b"\1" if bool(self.values.get(name, False) or False) else "\0")
|
||||
|
||||
# NOTE: IPADDR AND IPPORT USE THE BIG ENDIAN sUInt32 AND sUInt16
|
||||
# THESE ARE NOT FROM THE MESSAGE CLASS, THEY ARE FROM THE GLOBAL SCOPE!
|
||||
# IT IS INTENTIONAL!
|
||||
elif type == self.TYPE.IPADDR:
|
||||
handle.write(sUInt32.pack(int(ipaddress.IPv4Address(self.values.get(name, "0.0.0.0") or "0.0.0.0"))))
|
||||
|
||||
elif type == self.TYPE.IPPORT:
|
||||
handle.write(sUInt16.pack(int(self.values.get(name, 0) or 0)&0xFFFF))
|
||||
|
||||
else:
|
||||
raise Exception("Unknown type {}".format(type))
|
||||
|
||||
def fromStream(self, handle):
|
||||
for name, (type, size) in self.parameters.items():
|
||||
if type == self.TYPE.NULL:
|
||||
pass
|
||||
|
||||
elif type == self.TYPE.FIXED:
|
||||
data = handle.read(size)
|
||||
|
||||
elif type == self.TYPE.VARIABLE:
|
||||
if size == 1:
|
||||
data = handle.read(self.sVariable1.unpack(handle.read(self.sVariable1.size))[0])
|
||||
|
||||
elif size == 2:
|
||||
data = handle.read(self.sVariable2.unpack(handle.read(self.sVariable2.size))[0])
|
||||
|
||||
else:
|
||||
raise Exception("Invalid variable size {}".format(size))
|
||||
|
||||
elif type == self.TYPE.U8:
|
||||
data, = self.sU8.unpack(handle.read(self.sU8.size))
|
||||
|
||||
elif type == self.TYPE.U16:
|
||||
data, = self.sU16.unpack(handle.read(self.sU16.size))
|
||||
|
||||
elif type == self.TYPE.U32:
|
||||
data, = self.sU32.unpack(handle.read(self.sU32.size))
|
||||
|
||||
elif type == self.TYPE.U64:
|
||||
data, = self.sU64.unpack(handle.read(self.sU64.size))
|
||||
|
||||
elif type == self.TYPE.S8:
|
||||
data, = self.sS8.unpack(handle.read(self.sS8.size))
|
||||
|
||||
elif type == self.TYPE.S16:
|
||||
data, = self.sS16.unpack(handle.read(self.sS16.size))
|
||||
|
||||
elif type == self.TYPE.S32:
|
||||
data, = self.sS32.unpack(handle.read(self.sS32.size))
|
||||
|
||||
elif type == self.TYPE.S64:
|
||||
data, = self.sS64.unpack(handle.read(self.sS64.size))
|
||||
|
||||
elif type == self.TYPE.F32:
|
||||
data, = self.sF32.unpack(handle.read(self.sF32.size))
|
||||
|
||||
elif type == self.TYPE.F64:
|
||||
data, = self.sF64.unpack(handle.read(self.sF64.size))
|
||||
|
||||
elif type == self.TYPE.LLVECTOR3:
|
||||
data = self.sLLVector3.unpack(handle.read(self.sLLVector3.size))
|
||||
|
||||
elif type == self.TYPE.LLVECTOR3D:
|
||||
data = self.sLLVector3D.unpack(handle.read(self.sLLVector3D.size))
|
||||
|
||||
elif type == self.TYPE.LLVECTOR4:
|
||||
data = self.sLLVector4.unpack(handle.read(self.sLLVector4.size))
|
||||
|
||||
elif type == self.TYPE.LLQUATERNION:
|
||||
# NOTE: Quaternions are transmitted as vectors. The W component
|
||||
# is missing and is just generated on the fly.
|
||||
data = self.sLLVector3.unpack(handle.read(self.sLLVector3.size))
|
||||
|
||||
elif type == self.TYPE.LLUUID:
|
||||
data = uuid.UUID(bytes=handle.read(16))
|
||||
|
||||
elif type == self.TYPE.BOOL:
|
||||
data = handle.read(1)[0] != 0
|
||||
|
||||
# NOTE: IPADDR AND IPPORT USE THE BIG ENDIAN sUInt32 AND sUInt16
|
||||
# THESE ARE NOT FROM THE MESSAGE CLASS, THEY ARE FROM THE GLOBAL SCOPE!
|
||||
# IT IS INTENTIONAL!
|
||||
elif type == self.TYPE.IPADDR:
|
||||
data = ipaddress.IPv4Address(sUInt32.unpack(handle.read(sUInt32.size)[0]))
|
||||
|
||||
elif type == self.TYPE.IPPORT:
|
||||
data, = sUInt16.unpack(handle.read(sUInt16.size))
|
||||
|
||||
else:
|
||||
raise Exception("Unknown type {}".format(type))
|
||||
|
||||
self.values[name] = data
|
||||
|
||||
def registerParameter(self, name, type, size):
|
||||
self.parameters[name] = (type, size)
|
||||
|
||||
def copy(self):
|
||||
block = Block(self.name)
|
||||
block.parameters = self.parameters
|
||||
|
||||
return block
|
||||
|
||||
|
||||
class BlockArray(Block):
|
||||
def __init__(self, name, count = None):
|
||||
super().__init__(name)
|
||||
self.count = count
|
||||
self.blocks = []
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.name}[{self.count or len(self.blocks)}]>"
|
||||
|
||||
def __getitem__(self, i):
|
||||
if self.count != None and i > self.count:
|
||||
raise IndexError("block index out of range")
|
||||
|
||||
for _ in range(len(self.blocks), i + 1):
|
||||
self.blocks.append(Block(self.name))
|
||||
self.blocks[-1].parameters = self.parameters
|
||||
|
||||
return self.blocks[i]
|
||||
|
||||
def __len__(self):
|
||||
return self.count if self.count is not None else len(self.blocks)
|
||||
|
||||
def __iter__(self):
|
||||
for i in range(len(self)):
|
||||
yield self[i]
|
||||
|
||||
def toStream(self, handle):
|
||||
if self.count == None:
|
||||
handle.write(sUInt8.pack(self.count))
|
||||
|
||||
for i in range(self.count or len(self.blocks)):
|
||||
self[i].toStream(handle)
|
||||
|
||||
def fromStream(self, handle):
|
||||
count = self.count
|
||||
if count == None:
|
||||
count, = sUInt8.unpack(handle.read(sUInt8.size))
|
||||
|
||||
for i in range(count):
|
||||
self[i].fromStream(handle)
|
||||
|
||||
def copy(self):
|
||||
block = BlockArray(self.name, self.count)
|
||||
block.parameters = self.parameters
|
||||
|
||||
return block
|
||||
|
||||
class Message:
|
||||
class FREQUENCY(Enum):
|
||||
NULL = 0
|
||||
HIGH = 1
|
||||
MEDIUM = 2
|
||||
LOW = 4
|
||||
|
||||
class TRUST(Enum):
|
||||
TRUST = auto()
|
||||
NOTRUST = auto()
|
||||
|
||||
class ENCODING(Enum):
|
||||
UNENCODED = auto()
|
||||
ZEROCODED = auto()
|
||||
|
||||
class DEPRECATION(Enum):
|
||||
NOT = 0
|
||||
UDPDEPRECATED = 1
|
||||
UDPBLACKLISTED = 2
|
||||
DEPRECATED = 3
|
||||
|
||||
def __init__(self, name, frequency, id, trust = None, encoding = None, deprecation = None):
|
||||
self.name = name
|
||||
self.frequency = frequency
|
||||
self.id = id
|
||||
self.trust = trust or self.TRUST.TRUST
|
||||
self.encoding = encoding or self.ENCODING.UNENCODED
|
||||
self.blocks = OrderedDict()
|
||||
self.deprecation = deprecation or self.DEPRECATION.NOT
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.name} {self.frequency} {self.id} {self.trust} {self.encoding}>"
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.blocks[name]
|
||||
|
||||
def __bytes__(self):
|
||||
buf = io.BytesIO()
|
||||
self.toStream(buf, True)
|
||||
return buf.getvalue()
|
||||
|
||||
def toStream(self, handle, writeID = False):
|
||||
if writeID:
|
||||
if self.frequency == self.FREQUENCY.LOW:
|
||||
handle.write(sUInt32.pack(self.id))
|
||||
elif self.frequency == self.FREQUENCY.MEDIUM:
|
||||
handle.write(sUInt16.pack(self.id))
|
||||
elif self.frequency == self.FREQUENCY.HIGH:
|
||||
handle.write(sUInt8.pack(self.id))
|
||||
|
||||
for block in self.blocks.values():
|
||||
block.toStream(handle)
|
||||
|
||||
def load(self, handle, readID = False):
|
||||
if readID:
|
||||
# This doesn't do anything.
|
||||
# Perhaps we could verify the ID?
|
||||
if self.frequency == self.FREQUENCY.LOW:
|
||||
handle.read(sUInt32.size)
|
||||
elif self.frequency == self.FREQUENCY.MEDIUM:
|
||||
handle.read(sUInt16.size)
|
||||
elif self.frequency == self.FREQUENCY.HIGH:
|
||||
handle.read(sUInt8.size)
|
||||
|
||||
for block in self.blocks.values():
|
||||
block.fromStream(handle)
|
||||
|
||||
def loads(self, data, verifyID = True):
|
||||
handle = io.BytesIO(data)
|
||||
self.load(handle, verifyID)
|
||||
|
||||
def registerBlock(self, block):
|
||||
if block.name in self.blocks:
|
||||
raise Exception("Block {} already registered in message {}".format(block.name, self.name))
|
||||
self.blocks[block.name] = block
|
||||
|
||||
def copy(self):
|
||||
msg = Message(self.name, self.frequency, self.id, self.trust, self.encoding)
|
||||
for block in self.blocks.values():
|
||||
msg.registerBlock(block.copy())
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
class MessageTemplate:
|
||||
def __init__(self):
|
||||
self.messages = {}
|
||||
self.ids = {}
|
||||
|
||||
def registerMessage(self, message):
|
||||
self.messages[message.name] = message
|
||||
self.messages[message.id] = message
|
||||
|
||||
def getMessage(self, name):
|
||||
return self.messages[name].copy()
|
||||
|
||||
def loadMessage(self, message):
|
||||
if message[0] == 0xFF:
|
||||
if message[1] == 0xFF:
|
||||
mid, = sUInt32.unpack_from(message[0:4])
|
||||
|
||||
else:
|
||||
mid, = sUInt16.unpack_from(message[0:2])
|
||||
|
||||
else:
|
||||
mid = message[0]
|
||||
|
||||
msg = self.getMessage(mid)
|
||||
msg.loads(message)
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def load(cls, handle):
|
||||
templates = parseTemplateAbstract(handle.read())
|
||||
return cls.loadAst(templates)
|
||||
|
||||
@classmethod
|
||||
def loadAst(cls, templates):
|
||||
self = cls()
|
||||
if not templates:
|
||||
raise ValueError("Empty template specified!")
|
||||
|
||||
if templates.pop(0) != "version":
|
||||
raise Exception("Expected version as first parameter to message template!")
|
||||
|
||||
tVersion = float(templates.pop(0))
|
||||
|
||||
counters = {"High": 0, "Medium": 0, "Low": 0, "Fixed": 0}
|
||||
|
||||
for template in templates:
|
||||
if type(template) != list:
|
||||
raise Exception("Expected {} in template abstract, got {}!".format(type(list), type(template)))
|
||||
|
||||
mName = template.pop(0)
|
||||
if type(mName) != str:
|
||||
raise Exception("Expected name to be string")
|
||||
|
||||
mFrequency = template.pop(0)
|
||||
if type(mFrequency) != str:
|
||||
raise Exception("Expected frequency to be string")
|
||||
|
||||
if mFrequency not in counters.keys():
|
||||
raise Exception("Unknown frequency type {}".format(mFrequency))
|
||||
|
||||
mID = 0
|
||||
|
||||
# Version 2.0 and onward requires explicit message IDs
|
||||
counters[mFrequency] += 1
|
||||
if mFrequency == "Fixed" or tVersion >= 2.0:
|
||||
mID = template.pop(0)
|
||||
if type(mID) != str:
|
||||
raise Exception("Expected ID to be string")
|
||||
|
||||
if mID.startswith("0x"):
|
||||
mID = int(mID, 16)
|
||||
else:
|
||||
mID = int(mID)
|
||||
|
||||
else:
|
||||
mID = counters[mFrequency]
|
||||
|
||||
mTrust = template.pop(0)
|
||||
if type(mTrust) != str:
|
||||
raise Exception("Expected Trust to be string")
|
||||
|
||||
if mTrust not in ("Trusted", "NotTrusted"):
|
||||
raise Exception("Unknown trust type {}".format(mTrust))
|
||||
|
||||
mEncoding = "Unencoded"
|
||||
|
||||
if tVersion >= 2.0:
|
||||
mEncoding = template.pop(0)
|
||||
if type(mEncoding) != str:
|
||||
raise Exception("Expected Encoding to be string")
|
||||
|
||||
if mEncoding not in ("Unencoded", "Zerocoded"):
|
||||
raise Exception("Unknown encoding type {}".format(mEncoding))
|
||||
|
||||
# --- Start message construction ---
|
||||
|
||||
# Idk, this is how message.cpp does it
|
||||
if mFrequency == "Fixed":
|
||||
mFrequency = Message.FREQUENCY.LOW
|
||||
|
||||
elif mFrequency == "Low":
|
||||
if mID > 0xFFFF:
|
||||
raise Exception("Too many Low frequency messages")
|
||||
|
||||
mFrequency = Message.FREQUENCY.LOW
|
||||
mID = (0xFFFF << 16) | mID
|
||||
|
||||
elif mFrequency == "Medium":
|
||||
if mID > 0xFF:
|
||||
raise Exception("Too many Medium frequency messages")
|
||||
|
||||
mFrequency = Message.FREQUENCY.MEDIUM
|
||||
mID = (0xFF << 8) | mID
|
||||
|
||||
elif mFrequency == "High":
|
||||
mFrequency = Message.FREQUENCY.HIGH
|
||||
if mID > 0xFF:
|
||||
raise Exception("Too many high frequency messages")
|
||||
|
||||
|
||||
if mTrust == "Trusted":
|
||||
mTrust = Message.TRUST.TRUST
|
||||
|
||||
elif mTrust == "NotTrusted":
|
||||
mTrust = Message.TRUST.NOTRUST
|
||||
|
||||
|
||||
if mEncoding == "Unencoded":
|
||||
mEncoding = Message.ENCODING.UNENCODED
|
||||
|
||||
elif mEncoding == "Zerocoded":
|
||||
mEncoding = Message.ENCODING.ZEROCODED
|
||||
|
||||
mDeprecation = Message.DEPRECATION.NOT
|
||||
|
||||
if len(template):
|
||||
if template[0] == "UDPDeprecated":
|
||||
mDeprecation = Message.DEPRECATION.UDPDEPRECATED
|
||||
template.pop(0)
|
||||
elif template[0] == "UDPBlackListed":
|
||||
mDeprecation = Message.DEPRECATION.UDPBLACKLISTED
|
||||
template.pop(0)
|
||||
elif template[0] == "Deprecated":
|
||||
mDeprecation = Message.DEPRECATION.DEPRECATED
|
||||
template.pop(0)
|
||||
|
||||
message = Message(mName, mFrequency, mID, mTrust, mEncoding, mDeprecation)
|
||||
|
||||
# --- End message construction ---
|
||||
|
||||
# All that should remain now is blocks!
|
||||
for block in template:
|
||||
if len(block) < 2:
|
||||
raise Exception("Block must contain at least a name and quantity")
|
||||
|
||||
if type(block) != list:
|
||||
raise Exception("Expected block to be a list")
|
||||
|
||||
bName = block.pop(0)
|
||||
if type(bName) != str:
|
||||
raise Exception("Expected block name to be string")
|
||||
|
||||
bQuantity = block.pop(0)
|
||||
if type(bQuantity) != str:
|
||||
raise Exception("Expected quantity name to be string")
|
||||
|
||||
bCount = None
|
||||
if bQuantity == "Multiple":
|
||||
if len(block) < 1:
|
||||
raise Exception("Multiple quantity specified without count")
|
||||
|
||||
bCount = block.pop(0)
|
||||
if type(bQuantity) != str:
|
||||
raise Exception("Expected block count to be string")
|
||||
|
||||
bCount = int(bCount)
|
||||
|
||||
# --- Start block construction ---
|
||||
|
||||
mBlock = None
|
||||
if bQuantity in ("Multiple", "Variable"):
|
||||
mBlock = BlockArray(bName, bCount)
|
||||
elif bQuantity == "Single":
|
||||
mBlock = Block(bName)
|
||||
else:
|
||||
raise Exception("Unknown quantity {}".format(bQuantity))
|
||||
|
||||
# --- End block construction ---
|
||||
|
||||
# All that should remain now is parameters!
|
||||
for parameter in block:
|
||||
if len(parameter) < 2:
|
||||
raise Exception("Parameter must contain at least a name and type")
|
||||
|
||||
if type(parameter) != list:
|
||||
raise Exception("Expected parameter to be a list")
|
||||
|
||||
pName = parameter.pop(0)
|
||||
if type(pName) != str:
|
||||
raise Exception("Expected parameter name to be string")
|
||||
|
||||
pType = parameter.pop(0)
|
||||
if type(pType) != str:
|
||||
raise Exception("Expected parameter type to be string")
|
||||
|
||||
pSize = None
|
||||
if pType in ("Fixed", "Variable"):
|
||||
if len(parameter) < 1:
|
||||
raise Exception("Parameter specified Fixed or Variable without size")
|
||||
|
||||
pSize = parameter.pop(0)
|
||||
if type(pType) != str:
|
||||
raise Exception("Expected parameter size to be string")
|
||||
pSize = int(pSize)
|
||||
|
||||
pType = mBlock.TYPE[pType.upper()]
|
||||
|
||||
# --- Start parameter construction ---
|
||||
mBlock.registerParameter(pName, pType, pSize)
|
||||
# --- End parameter construction ---
|
||||
|
||||
|
||||
message.registerBlock(mBlock)
|
||||
|
||||
self.registerMessage(message)
|
||||
return self
|
||||
|
||||
|
||||
def parseTemplateAbstract(text):
|
||||
parsed = []
|
||||
stack = [parsed]
|
||||
strbuf = ""
|
||||
comment = 0
|
||||
for c in text:
|
||||
if c == "/":
|
||||
comment += 1
|
||||
continue
|
||||
|
||||
elif comment >= 2:
|
||||
if c == "\n":
|
||||
comment = 0
|
||||
else:
|
||||
continue
|
||||
|
||||
elif comment == 1:
|
||||
raise Exception("Unexpected /")
|
||||
|
||||
if c in (" ", "\t", "{", "}", "\n"):
|
||||
if strbuf != "":
|
||||
stack[-1].append(strbuf)
|
||||
strbuf = ""
|
||||
|
||||
if c == "{":
|
||||
stack.append([])
|
||||
|
||||
elif c == "}":
|
||||
tmp = stack.pop()
|
||||
stack[-1].append(tmp)
|
||||
|
||||
elif not c in (" ", "\t", "\n"):
|
||||
strbuf += c
|
||||
|
||||
return parsed
|
||||
|
||||
__templateCache = None
|
||||
|
||||
def getDefaultTemplate():
|
||||
global __templateCache
|
||||
if not __templateCache:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
template_path = os.path.join(script_dir, "message_template.msg")
|
||||
with open(template_path, "r") as f:
|
||||
__templateCache = MessageTemplate.load(f)
|
||||
return __templateCache
|
||||
|
||||
def unitTest():
|
||||
with open("message_template.msg", "r") as f:
|
||||
template = MessageTemplate.load(f)
|
||||
msg = template.getMessage("TestMessage")
|
||||
msg.TestBlock1.Test1 = 4
|
||||
|
||||
for i in range(0, 4):
|
||||
msg.NeighborBlock[i].Test0 = i * 3
|
||||
msg.NeighborBlock[i].Test1 = i * 3 + 1
|
||||
msg.NeighborBlock[i].Test2 = i * 3 + 2
|
||||
print(msg)
|
||||
print(ZeroEncode(bytes(msg)))
|
||||
print(ZeroDecode(ZeroEncode(bytes(msg))))
|
||||
print(bytes(msg) == ZeroDecode(ZeroEncode(bytes(msg))))
|
||||
msg2 = template.getMessage("TestMessage")
|
||||
msg2.loads(bytes(msg))
|
||||
print(msg2.TestBlock1.Test1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unitTest()
|
||||
181
pymetaverse/packet.py
Normal file
181
pymetaverse/packet.py
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
import struct
|
||||
import io
|
||||
|
||||
# https://wiki.secondlife.com/wiki/Packet_Layout
|
||||
"""
|
||||
+-+-+-+-+----+--------+--------+--------+--------+--------+-----...-----+
|
||||
|Z|R|R|A| | | | Extra |
|
||||
|E|E|E|C| | Sequence number (4 bytes) | Extra | Header |
|
||||
|R|L|S|K| | | (byte) | (N bytes) |
|
||||
+-+-+-+-+----+--------+--------+--------+--------+--------+-----...-----+
|
||||
"""
|
||||
|
||||
def zeroEncode(buf):
|
||||
output = io.BytesIO()
|
||||
count = None
|
||||
i = 0
|
||||
l = len(buf)
|
||||
count = 0
|
||||
while i < l:
|
||||
if buf[i] == 0:
|
||||
count = 0
|
||||
while i < l and buf[i] == 0:
|
||||
count += 1
|
||||
i += 1
|
||||
if count == 255:
|
||||
output.write(bytes([0, 0xFF]))
|
||||
count = 0
|
||||
if count != 0:
|
||||
output.write(bytes([0, count]))
|
||||
if i < l:
|
||||
output.write(bytes([buf[i]]))
|
||||
i += 1
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
def zeroDecode(buf):
|
||||
output = io.BytesIO()
|
||||
count = None
|
||||
i = 0
|
||||
l = len(buf)
|
||||
while i < l:
|
||||
if buf[i] == 0:
|
||||
i += 1
|
||||
output.write(bytes(buf[i]))
|
||||
else:
|
||||
output.write(bytes([buf[i]]))
|
||||
i += 1
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
class Packet:
|
||||
MTU = 1400
|
||||
|
||||
sPacketHeader = struct.Struct(">BIB")
|
||||
sPacketAcks = struct.Struct(">I")
|
||||
|
||||
class FLAGS:
|
||||
ZEROCODE = 0x80
|
||||
RELIABLE = 0x40
|
||||
RESENT = 0x20
|
||||
ACK = 0x10
|
||||
|
||||
def __init__(self, seq, body = None, flags = None, acks = None, extra = None):
|
||||
self.sequence = seq
|
||||
self.flags = flags or 0
|
||||
self.extra = extra or b""
|
||||
self.body = body or b""
|
||||
self.acks = acks or []
|
||||
|
||||
@property
|
||||
def reliable(self):
|
||||
return bool(self.flags & self.FLAGS.RELIABLE)
|
||||
|
||||
@reliable.setter
|
||||
def reliable(self, value):
|
||||
if value:
|
||||
self.flags |= self.FLAGS.RELIABLE
|
||||
else:
|
||||
self.flags &= ~self.FLAGS.RELIABLE
|
||||
|
||||
@property
|
||||
def resent(self):
|
||||
return bool(self.flags & self.FLAGS.RESENT)
|
||||
|
||||
@resent.setter
|
||||
def resent(self, value):
|
||||
if value:
|
||||
self.flags |= self.FLAGS.RESENT
|
||||
else:
|
||||
self.flags &= ~self.FLAGS.RESENT
|
||||
|
||||
@property
|
||||
def zerocode(self):
|
||||
return bool(self.flags & self.FLAGS.ZEROCODE)
|
||||
|
||||
@zerocode.setter
|
||||
def zerocode(self, value):
|
||||
if value:
|
||||
self.flags |= self.FLAGS.ZEROCODE
|
||||
else:
|
||||
self.flags &= ~self.FLAGS.ZEROCODE
|
||||
|
||||
def toBytes(self):
|
||||
output = io.BytesIO()
|
||||
flags = self.flags
|
||||
if len(self.acks) > 0:
|
||||
flags = flags | self.FLAGS.ACK
|
||||
|
||||
output.write(self.sPacketHeader.pack(flags, self.sequence, len(self.extra)))
|
||||
output.write(self.extra)
|
||||
|
||||
if flags & self.FLAGS.ZEROCODE:
|
||||
output.write(zeroEncode(self.body))
|
||||
else:
|
||||
output.write(self.body)
|
||||
|
||||
if flags & self.FLAGS.ACK:
|
||||
count = 0
|
||||
while len(self.acks):
|
||||
size = output.tell()
|
||||
if size - 5 >= self.MTU:
|
||||
break
|
||||
|
||||
if count >= 255:
|
||||
break
|
||||
|
||||
output.write(self.sPacketAcks.pack(self.acks.pop(0)))
|
||||
count += 1
|
||||
|
||||
output.write(bytes([count]))
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
def __bytes__(self):
|
||||
return self.toByte()
|
||||
|
||||
@classmethod
|
||||
def fromBytes(cls, data):
|
||||
return cls.fromStream(io.BytesIO(data))
|
||||
|
||||
@classmethod
|
||||
def fromStream(cls, f):
|
||||
flags, seq, extra = cls.sPacketHeader.unpack_from(f.read(cls.sPacketHeader.size))
|
||||
|
||||
if extra > 0:
|
||||
extra = f.read(extra)
|
||||
else:
|
||||
extra = b""
|
||||
|
||||
bodyStart = f.tell()
|
||||
|
||||
body = None
|
||||
acks = []
|
||||
if flags & cls.FLAGS.ACK:
|
||||
f.seek(-1, io.SEEK_END)
|
||||
ackCount, = f.read(1)
|
||||
|
||||
f.seek(-(ackCount * cls.sPacketAcks.size) - 1, io.SEEK_END)
|
||||
|
||||
bodyEnd = f.tell()
|
||||
|
||||
acks = [None] * ackCount
|
||||
for i in range(ackCount):
|
||||
acks[i], = cls.sPacketAcks.unpack(f.read(cls.sPacketAcks.size))
|
||||
|
||||
f.seek(bodyStart)
|
||||
body = f.read(bodyEnd - bodyStart)
|
||||
|
||||
else:
|
||||
body = f.read()
|
||||
|
||||
if flags & cls.FLAGS.ZEROCODE:
|
||||
body = zeroDecode(body)
|
||||
|
||||
return cls(seq, body, flags = flags, acks = acks, extra = extra)
|
||||
|
||||
@classmethod
|
||||
def fromBytes(cls, data):
|
||||
return cls.fromStream(io.BytesIO(data))
|
||||
|
||||
0
pymetaverse/region.py
Normal file
0
pymetaverse/region.py
Normal file
62
pymetaverse/simulator.py
Normal file
62
pymetaverse/simulator.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from .circuit import Circuit
|
||||
from . import messages
|
||||
from . import httpclient
|
||||
from . import llsd
|
||||
from .eventtarget import EventTarget
|
||||
|
||||
class Simulator(EventTarget):
|
||||
def __init__(self, agent):
|
||||
super().__init__()
|
||||
self.agent = agent
|
||||
self.host = None
|
||||
self.circuit = None
|
||||
self.region = None
|
||||
self.caps = {}
|
||||
self.messageTemplate = messages.getDefaultTemplate()
|
||||
|
||||
async def connect(self, host, circuitCode):
|
||||
self.host = host
|
||||
self.circuit = await Circuit.create(host)
|
||||
self.circuit.on("message", self.handleMessage)
|
||||
msg = self.messageTemplate.getMessage("UseCircuitCode")
|
||||
msg.CircuitCode.Code = circuitCode
|
||||
msg.CircuitCode.SessionID = self.agent.sessionId
|
||||
msg.CircuitCode.ID = self.agent.agentId
|
||||
self.circuit.send(msg, True)
|
||||
|
||||
def send(self, msg, reliable = False):
|
||||
self.circuit.send(msg, reliable)
|
||||
|
||||
def handleSystemMessages(self, msg):
|
||||
if msg.name == "PacketAck":
|
||||
acks = []
|
||||
for ack in msg.Packets:
|
||||
acks.append(ack.ID)
|
||||
|
||||
self.circuit.acknowledge(acks)
|
||||
|
||||
elif msg.name == "StartPingCheck":
|
||||
msg = self.messageTemplate.getMessage("CompletePingCheck")
|
||||
msg.PingID.PingID = msg.PingID.PingID
|
||||
self.send(msg)
|
||||
|
||||
elif msg.name == "RegionHandshake":
|
||||
msg = self.messageTemplate.getMessage("RegionHandshakeReply")
|
||||
msg.AgentData.AgentID = self.agent.agentId
|
||||
msg.AgentData.SessionID = self.agent.sessionId
|
||||
msg.RegionInfo.Flags = 2
|
||||
self.send(msg, True)
|
||||
|
||||
def handleMessage(self, addr, body):
|
||||
# Reject unknown hosts as a security precaution
|
||||
if addr != self.host:
|
||||
print("REJECT")
|
||||
return
|
||||
|
||||
msg = self.messageTemplate.loadMessage(body)
|
||||
self.handleSystemMessages(msg)
|
||||
self.fire("message", msg)
|
||||
|
||||
async def fetchCapabilities(self, seed):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user