From 1700fadfbf4ba37ad094f1d46629f14dcaadd49c Mon Sep 17 00:00:00 2001 From: Kyler Eastridge Date: Sat, 28 Jun 2025 07:33:08 -0400 Subject: [PATCH] Add example bots --- examples/bots/complexBot/bots.json | 9 ++ examples/bots/complexBot/defaultFuncs.py | 23 +++ examples/bots/complexBot/main.py | 176 +++++++++++++++++++++++ examples/bots/simpleBot.py | 33 +++++ 4 files changed, 241 insertions(+) create mode 100644 examples/bots/complexBot/bots.json create mode 100644 examples/bots/complexBot/defaultFuncs.py create mode 100644 examples/bots/complexBot/main.py create mode 100644 examples/bots/simpleBot.py diff --git a/examples/bots/complexBot/bots.json b/examples/bots/complexBot/bots.json new file mode 100644 index 0000000..b815335 --- /dev/null +++ b/examples/bots/complexBot/bots.json @@ -0,0 +1,9 @@ +[ + { + "username": ["bot", "example"], + "password": "super secret", + "functions": [ + "defaultFuncs:logChat" + ] + } +] \ No newline at end of file diff --git a/examples/bots/complexBot/defaultFuncs.py b/examples/bots/complexBot/defaultFuncs.py new file mode 100644 index 0000000..6e43ebc --- /dev/null +++ b/examples/bots/complexBot/defaultFuncs.py @@ -0,0 +1,23 @@ +import datetime +import uuid +from aiohttp import web + +def logChat(instance, bot): + @bot.on("message", name="ChatFromSimulator") + def ChatFromSimulator(simulator, message): + # Ignore start / stop + if message.ChatData.Audible == 0 or message.ChatData.ChatType in (4, 5): + return + + text = message.ChatData.Message.rstrip(b"\0").decode() + print("[{}] [{}] {}: {}".format( + datetime.datetime.now().strftime("%Y-%M-%d %H:%m:%S"), + ".".join(bot.agent.username), + message.ChatData.FromName.rstrip(b"\0").decode(), + text + )) + +def testRoute(instance, bot): + @instance.route("test") + async def testRoute(request): + return web.Response(text=f"Got request") diff --git a/examples/bots/complexBot/main.py b/examples/bots/complexBot/main.py new file mode 100644 index 0000000..109c2e8 --- /dev/null +++ b/examples/bots/complexBot/main.py @@ -0,0 +1,176 @@ +import logging + +logging.basicConfig( + level=logging.INFO, # Or INFO, WARNING, etc. + format="%(asctime)s [%(levelname)s] %(message)s" +) + +import asyncio +import json +import time +import datetime +import traceback +import uuid +import re +import importlib +from aiohttp import web +from pymetaverse import login +from pymetaverse.bot import SimpleBot +from pymetaverse.viewer import messages +from pymetaverse.const import * + +def loadDictIntoMessage(msg, data): + for name, block in data.items(): + if type(data[name]) == list: + for i, varblock in enumerate(data[name]): + for key, value in data[name][i].items(): + if type(value) == dict: + if value["type"] == "bytestring": + value = value["data"].encode() + b"\0" + elif value["type"] == "bytes": + value = bytes(value["data"]) + msg.blocks[name][i].values[key] = value + msg.blocks[name].count = len(data[name]) + + elif type(data[name]) == dict: + for key, value in data[name].items(): + if type(value) == dict: + if value["type"] == "bytestring": + value = value["data"].encode() + b"\0" + elif value["type"] == "bytes": + value = bytes(value["data"]) + msg.blocks[name].values[key] = value + +class BotInstance: + def __init__(self, username, password, features = None): + self.username = username + self.password = password + self.features = features or [] + self.routes = [] + self.bot = None + + def route(self, pattern): + def decorator(func): + regex = re.compile(f"^{pattern}$") + self.routes.append((regex, func)) + return func + return decorator + + async def handle_request(self, request, path): + for regex, handler in self.routes: + match = regex.match(path) + if match: + return await handler(request, **match.groupdict()) + logging.warning("[{}] No handler for path: {}".format( + ".".join(self.bot.agent.username), + path + )) + return web.Response(status=404, text="No handler for path") + + async def run(self): + while True: + try: + bot = SimpleBot() + self.bot = bot + self.routes = [] + for feature in self.features: + feature(self, bot) + await bot.login(self.username, self.password) + logging.info("[{}] Logged in.".format( + ".".join(bot.agent.username) + )) + await bot.run() + logging.info("[{}] Logged out.".format( + ".".join(bot.agent.username) + )) + self.bot = None + except asyncio.exceptions.CancelledError as e: + break + +def load_callable(path: str): + """Loads a function or object from a string like 'package.module:func'.""" + if ':' not in path: + raise ValueError(f"Invalid path '{path}', expected format 'module.submodule:function'") + + module_path, func_name = path.split(':', 1) + module = importlib.import_module(module_path) + + try: + return getattr(module, func_name) + except AttributeError: + raise ImportError(f"Function '{func_name}' not found in module '{module_path}'") + +async def main(): + with open("bots.json", "r") as f: + bots = json.load(f) + + instances = [] + for bot in bots: + if bot.get("disabled", False) == True: + continue + + functions = [] + for function_path in bot.get("functions", []): + functions.append(load_callable(function_path)) + instance = BotInstance(bot["username"], bot["password"], functions) + instances.append(instance) + + async def handle_bot_request(request): + uuid_str = request.match_info['uuid'] + path = request.match_info.get('path', "") + + bot = None + try: + bot_uuid = str(uuid.UUID(uuid_str)) + for instance in instances: + try: + if instance.bot.agent.agentId == uuid_str: + bot = instance + break + except Exception as e: + raise e + + except ValueError: + for instance in instances: + try: + if ".".join(instance.bot.agent.username).lower() == uuid_str.lower(): + bot = instance + break + except Exception as e: + raise e + + if bot: + return await bot.handle_request(request, path) + + logging.warning("[WEB] No bot found: {}".format( + uuid_str + )) + return web.Response(status=404, text="Bot not found") + + async def handle_bot_index(request): + response = {} + for instance in instances: + response[instance.bot.agent.agentId] = { + "username": list(instance.bot.agent.username) if instance.bot.agent.username != (None, None) else [] + } + + return web.Response(status=200, text=json.dumps(response)) + + async def run_web_server(): + app = web.Application() + app.router.add_get('/bot/{uuid}/{path:.*}', handle_bot_request) + app.router.add_get('/bot/{uuid}', handle_bot_request) + app.router.add_get('/bot', handle_bot_index) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, 'localhost', 26875) + await site.start() + + # Launch both the web server and bots + await asyncio.gather( + run_web_server(), + *[instance.run() for instance in instances] + ) + +# Run everything +asyncio.run(main()) diff --git a/examples/bots/simpleBot.py b/examples/bots/simpleBot.py new file mode 100644 index 0000000..279f1c8 --- /dev/null +++ b/examples/bots/simpleBot.py @@ -0,0 +1,33 @@ +import asyncio +import datetime +from pymetaverse import login +from pymetaverse.bot import SimpleBot +from pymetaverse.const import * + +bot = SimpleBot() + +@bot.on("message", name="ChatFromSimulator") +def ChatFromSimulator(simulator, message): + # Ignore start / stop + if message.ChatData.ChatType in (4, 5): + return + + sender = message.ChatData.FromName.rstrip(b"\0").decode() + text = message.ChatData.Message.rstrip(b"\0").decode() + + if text == "logout": + bot.say(0, "Ok!") + bot.logout() + + print("[{}] {}: {}".format( + datetime.datetime.now().strftime("%Y-%M-%d %H:%m:%S"), + sender, + text + )) + +async def main(): + await bot.login(("Example", "Resident"), "password") + await bot.run() + +# Run everything +asyncio.run(main()) \ No newline at end of file