mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-19 19:22:49 +00:00
feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
276
tools/chat-bridge/tests/test_core.py
Normal file
276
tools/chat-bridge/tests/test_core.py
Normal file
@@ -0,0 +1,276 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from chat_bridge.adapters.console import parse_line # noqa: E402
|
||||
from chat_bridge.adapters.webhook import webhook_body # noqa: E402
|
||||
from chat_bridge.bridge import Bridge, Link, load_state, save_state # noqa: E402
|
||||
from chat_bridge.config import Config, LinkConfig, expand_env, parse_config # noqa: E402
|
||||
from chat_bridge.core import (Cursor, GameFilter, GameMessage, RateLimiter, RemoteMessage, game_name, # noqa: E402
|
||||
is_own_message, plain_line, send_body, should_relay)
|
||||
|
||||
|
||||
def row(id, channel="zone", sender="Bob", message="hi", zone=1200, instance=2, account=5, blocked=False, zone_name="Nimbus Station"):
|
||||
return {"id": id, "channel": channel, "sender_name": sender, "message": message, "zone_id": zone, "zone_name": zone_name,
|
||||
"instance_id": instance, "account_id": account, "blocked": blocked, "time": 0}
|
||||
|
||||
|
||||
def msg(**kw):
|
||||
return GameMessage.from_json(row(kw.pop("id", 1), **kw))
|
||||
|
||||
|
||||
class RelayRules(unittest.TestCase):
|
||||
def test_zone_chat_by_default(self):
|
||||
f = GameFilter()
|
||||
self.assertTrue(should_relay(msg(), f, "Discord"))
|
||||
self.assertFalse(should_relay(msg(channel="web", sender="[Web] admin"), f, "Discord"))
|
||||
|
||||
def test_private_chat_never_without_opt_in(self):
|
||||
self.assertFalse(should_relay(msg(channel="whisper"), GameFilter(channels=frozenset({"zone", "whisper"})), "Discord"))
|
||||
self.assertFalse(should_relay(msg(channel="team"), GameFilter(channels=frozenset({"team"})), "Discord"))
|
||||
with self.assertRaises(ValueError):
|
||||
GameFilter.from_config({"channels": ["zone", "whisper"]})
|
||||
f = GameFilter.from_config({"channels": ["whisper"], "allow_private": True})
|
||||
self.assertTrue(should_relay(msg(channel="whisper"), f, "Discord"))
|
||||
|
||||
def test_blocked_messages_are_not_relayed(self):
|
||||
self.assertFalse(should_relay(msg(blocked=True), GameFilter(), "Discord"))
|
||||
|
||||
def test_zone_and_instance_filters(self):
|
||||
f = GameFilter(zones=frozenset({1100}))
|
||||
self.assertFalse(should_relay(msg(zone=1200), f, "Discord"))
|
||||
self.assertTrue(should_relay(msg(zone=1100), f, "Discord"))
|
||||
f = GameFilter(zones=frozenset({1200}), instances=frozenset({3}))
|
||||
self.assertFalse(should_relay(msg(zone=1200, instance=2), f, "Discord"))
|
||||
|
||||
def test_empty_messages_skipped(self):
|
||||
self.assertFalse(should_relay(msg(message=" "), GameFilter(), "Discord"))
|
||||
|
||||
def test_server_channel(self):
|
||||
self.assertEqual(GameFilter().server_channel(), "zone")
|
||||
self.assertEqual(GameFilter(channels=frozenset({"zone", "web"})).server_channel(), "")
|
||||
|
||||
|
||||
class LoopPrevention(unittest.TestCase):
|
||||
def test_own_label_is_ignored(self):
|
||||
f = GameFilter(channels=frozenset({"zone", "web"}))
|
||||
own = msg(channel="web", sender="[Discord] Alice", account=99)
|
||||
self.assertTrue(is_own_message(own, "Discord"))
|
||||
self.assertFalse(should_relay(own, f, "Discord"))
|
||||
|
||||
def test_own_account_is_ignored(self):
|
||||
f = GameFilter(channels=frozenset({"zone", "web"}))
|
||||
other_label = msg(channel="web", sender="[Matrix] Carol", account=99)
|
||||
self.assertFalse(should_relay(other_label, f, "Discord", own_account_id=99))
|
||||
self.assertTrue(should_relay(other_label, f, "Discord", own_account_id=7))
|
||||
|
||||
def test_player_named_like_a_label_is_not_own(self):
|
||||
# Only the web channel carries labels; a player can't produce one
|
||||
self.assertFalse(is_own_message(msg(channel="zone", sender="[Discord] x"), "Discord"))
|
||||
|
||||
|
||||
class Formatting(unittest.TestCase):
|
||||
def test_plain_line(self):
|
||||
self.assertEqual(plain_line(msg()), "[Nimbus Station] Bob: hi")
|
||||
self.assertEqual(plain_line(msg(zone=0, zone_name="")), "Bob: hi")
|
||||
self.assertEqual(plain_line(msg(zone=1234, zone_name="")), "[zone 1234] Bob: hi")
|
||||
|
||||
def test_send_body(self):
|
||||
body = send_body(RemoteMessage("Alice", "hello\nthere"), "Discord", GameFilter())
|
||||
self.assertEqual(body, {"message": "hello there", "name": "Alice", "label": "Discord"})
|
||||
|
||||
def test_send_body_targets(self):
|
||||
body = send_body(RemoteMessage("A", "x"), "Discord", GameFilter(send_zone=1200, send_instance=2))
|
||||
self.assertEqual((body["zone"], body["instance"]), (1200, 2))
|
||||
body = send_body(RemoteMessage("A", "x"), "Discord", GameFilter(send_instance=2))
|
||||
self.assertNotIn("zone", body)
|
||||
self.assertNotIn("instance", body)
|
||||
|
||||
def test_send_body_limits(self):
|
||||
body = send_body(RemoteMessage("[mod] " + "n" * 50, "x" * 400), "Discord", GameFilter())
|
||||
self.assertEqual(len(body["message"]), 300)
|
||||
self.assertTrue(body["message"].endswith("..."))
|
||||
self.assertLessEqual(len(body["name"]), 32)
|
||||
self.assertNotIn("[", body["name"])
|
||||
self.assertIsNone(send_body(RemoteMessage("A", " \n "), "Discord", GameFilter()))
|
||||
with self.assertRaises(ValueError):
|
||||
send_body(RemoteMessage("A", "x"), "Bad[label]", GameFilter())
|
||||
|
||||
def test_game_name_fallback(self):
|
||||
self.assertEqual(game_name(" \n"), "someone")
|
||||
|
||||
def test_console_parse(self):
|
||||
self.assertEqual(parse_line("Alice: hi there\n", "Console"), RemoteMessage("Alice", "hi there"))
|
||||
self.assertEqual(parse_line("just text\n", "Console"), RemoteMessage("Console", "just text"))
|
||||
self.assertIsNone(parse_line(" \n", "Console"))
|
||||
|
||||
def test_webhook_body(self):
|
||||
body = webhook_body(msg(id=42), skipped=3)
|
||||
self.assertEqual(body["text"], "[Nimbus Station] Bob: hi")
|
||||
self.assertEqual((body["id"], body["skipped"]), (42, 3))
|
||||
|
||||
|
||||
class Resume(unittest.TestCase):
|
||||
def test_take_in_order_and_advance(self):
|
||||
c = Cursor(10)
|
||||
out = c.take([row(12), row(11), row(13)])
|
||||
self.assertEqual([m.id for m in out], [11, 12, 13])
|
||||
self.assertEqual(c.last_id, 13)
|
||||
|
||||
def test_no_duplicates_across_overlapping_pages(self):
|
||||
c = Cursor(10)
|
||||
c.take([row(11), row(12)])
|
||||
out = c.take([row(12), row(13)])
|
||||
self.assertEqual([m.id for m in out], [13])
|
||||
|
||||
def test_page_params(self):
|
||||
c = Cursor(7)
|
||||
self.assertEqual(c.page_params(500, "zone"), {"after": "7", "limit": "500", "channel": "zone"})
|
||||
self.assertEqual(c.page_params(500), {"after": "7", "limit": "500"})
|
||||
|
||||
def test_state_file_roundtrip(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "s.json")
|
||||
self.assertEqual(load_state(path), 0)
|
||||
save_state(path, 1234)
|
||||
self.assertEqual(load_state(path), 1234)
|
||||
|
||||
|
||||
class FakeDashboard:
|
||||
"""Serves GET /api/chat from a list, like the server: after=, limit=, channel=."""
|
||||
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
self.sent = []
|
||||
|
||||
async def chat_page(self, params):
|
||||
after, limit = int(params["after"]), int(params["limit"])
|
||||
rows = [r for r in self.rows if r["id"] > after and (not params.get("channel") or r["channel"] == params["channel"])][:limit]
|
||||
return {"messages": rows, "last_id": max([after] + [r["id"] for r in rows])}
|
||||
|
||||
async def send_chat(self, body):
|
||||
self.sent.append(body)
|
||||
return {"requestId": 1}
|
||||
|
||||
|
||||
class Recorder:
|
||||
name = "rec"
|
||||
|
||||
def __init__(self):
|
||||
self.got = []
|
||||
|
||||
async def send(self, m, skipped=0):
|
||||
self.got.append(m)
|
||||
|
||||
|
||||
class BridgeFlow(unittest.TestCase):
|
||||
def make(self, rows, state, game=None, per_min=0):
|
||||
cfg = Config(url="x", token="t", state_file=state)
|
||||
link = Link(LinkConfig("rec", "Discord", game or GameFilter(), {}, per_min, per_min), Recorder())
|
||||
dash = FakeDashboard(rows)
|
||||
return Bridge(cfg, dash, [link]), link, dash
|
||||
|
||||
def test_resume_after_restart(self):
|
||||
rows = [row(i) for i in range(1, 8)]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
state = os.path.join(d, "s.json")
|
||||
save_state(state, 3)
|
||||
bridge, link, _ = self.make(rows, state)
|
||||
asyncio.run(bridge.fetch())
|
||||
self.assertEqual([m.id for m, _ in self.drain(link)], [4, 5, 6, 7])
|
||||
self.assertEqual(load_state(state), 7)
|
||||
# A second read (e.g. the WebSocket woke it up again) repeats nothing
|
||||
asyncio.run(bridge.fetch())
|
||||
self.assertEqual(self.drain(link), [])
|
||||
# New messages after a restart (a new Bridge reading the saved state)
|
||||
rows.append(row(8))
|
||||
bridge2, link2, _ = self.make(rows, state)
|
||||
asyncio.run(bridge2.fetch())
|
||||
self.assertEqual([m.id for m, _ in self.drain(link2)], [8])
|
||||
|
||||
def test_filtered_rows_still_move_the_cursor(self):
|
||||
rows = [row(1, channel="whisper"), row(2, blocked=True), row(3, channel="web", sender="[Discord] me")]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
# Two channels: the server sends everything and the bridge drops what it mustn't relay
|
||||
bridge, link, _ = self.make(rows, os.path.join(d, "s.json"), game=GameFilter(channels=frozenset({"zone", "web"})))
|
||||
asyncio.run(bridge.fetch())
|
||||
self.assertEqual(self.drain(link), [])
|
||||
self.assertEqual(bridge.cursor.last_id, 3)
|
||||
|
||||
def test_pages_through_a_backlog(self):
|
||||
rows = [row(i) for i in range(1, 1201)]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
state = os.path.join(d, "s.json")
|
||||
save_state(state, 1)
|
||||
bridge, link, _ = self.make(rows, state)
|
||||
link.queue = asyncio.Queue() # unbounded, to count them all
|
||||
asyncio.run(bridge.fetch())
|
||||
self.assertEqual(len(self.drain(link)), 1199)
|
||||
|
||||
def test_rate_limit_to_remote(self):
|
||||
rows = [row(i) for i in range(1, 30)]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
state = os.path.join(d, "s.json")
|
||||
save_state(state, 1)
|
||||
bridge, link, _ = self.make(rows, state, per_min=6)
|
||||
asyncio.run(bridge.fetch())
|
||||
self.assertEqual(len(self.drain(link)), 5) # the burst
|
||||
self.assertGreater(link.out_limit.dropped, 0)
|
||||
|
||||
def test_deliver_posts_with_label(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
bridge, link, dash = self.make([], os.path.join(d, "s.json"), game=GameFilter(send_zone=1200))
|
||||
asyncio.run(bridge.deliverer(link)(RemoteMessage("Alice", "hey")))
|
||||
self.assertEqual(dash.sent, [{"message": "hey", "name": "Alice", "label": "Discord", "zone": 1200}])
|
||||
|
||||
@staticmethod
|
||||
def drain(link):
|
||||
out = []
|
||||
while not link.queue.empty():
|
||||
out.append(link.queue.get_nowait())
|
||||
return out
|
||||
|
||||
|
||||
class Limits(unittest.TestCase):
|
||||
def test_token_bucket(self):
|
||||
r = RateLimiter(per_minute=60, burst=2)
|
||||
self.assertTrue(r.allow(0.0))
|
||||
self.assertTrue(r.allow(0.0))
|
||||
self.assertFalse(r.allow(0.0))
|
||||
self.assertTrue(r.allow(1.0)) # one a second comes back
|
||||
self.assertEqual(r.take_dropped(), 1)
|
||||
self.assertEqual(r.dropped, 0)
|
||||
|
||||
def test_zero_is_unlimited(self):
|
||||
r = RateLimiter(per_minute=0)
|
||||
self.assertTrue(all(r.allow(0.0) for _ in range(100)))
|
||||
|
||||
|
||||
class ConfigParsing(unittest.TestCase):
|
||||
def test_env_expansion(self):
|
||||
env = {"TOK": "abc"}
|
||||
self.assertEqual(expand_env({"a": "$TOK", "b": ["${TOK}", "x$TOK"]}, env), {"a": "abc", "b": ["abc", "x$TOK"]})
|
||||
|
||||
def test_parse(self):
|
||||
cfg = parse_config({"dashboard": {"token": "$T"}, "links": [{"adapter": "console", "console": {"name": "N"}}]},
|
||||
environ={"T": "tok"}, default_labels={"console": "Console"})
|
||||
self.assertEqual(cfg.token, "tok")
|
||||
self.assertEqual(cfg.links[0].label, "Console")
|
||||
self.assertEqual(cfg.links[0].game.channels, frozenset({"zone"}))
|
||||
self.assertEqual(cfg.links[0].settings, {"name": "N"})
|
||||
|
||||
def test_env_overrides_and_errors(self):
|
||||
cfg = parse_config({"links": [{"adapter": "console"}]}, environ={"DLU_API_TOKEN": "x", "DLU_DASHBOARD_URL": "https://d"})
|
||||
self.assertEqual((cfg.url, cfg.token), ("https://d", "x"))
|
||||
with self.assertRaises(ValueError):
|
||||
parse_config({"links": [{"adapter": "console"}]}, environ={})
|
||||
with self.assertRaises(ValueError):
|
||||
parse_config({"dashboard": {"token": "x"}, "links": [{"adapter": "console", "label": "[bad]"}]}, environ={})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
80
tools/chat-bridge/tests/test_discord.py
Normal file
80
tools/chat-bridge/tests/test_discord.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from chat_bridge.adapters.discord import (DiscordAdapter, display_name, escape_markdown, from_discord, # noqa: E402
|
||||
should_forward, to_discord, webhook_username)
|
||||
from chat_bridge.core import GameMessage # noqa: E402
|
||||
|
||||
|
||||
def game(**kw):
|
||||
base = dict(id=1, channel="zone", sender_name="Bob", message="hi", zone_id=1200, zone_name="Nimbus Station")
|
||||
base.update(kw)
|
||||
return GameMessage(**base)
|
||||
|
||||
|
||||
class ToDiscord(unittest.TestCase):
|
||||
def test_bot_message(self):
|
||||
body = to_discord(game(message="*bold* @everyone"), via_webhook=False)
|
||||
self.assertEqual(body["content"], "**Bob** (Nimbus Station): \\*bold\\* @everyone")
|
||||
self.assertEqual(body["allowed_mentions"], {"parse": []}) # nobody is pinged
|
||||
|
||||
def test_webhook_message(self):
|
||||
body = to_discord(game(sender_name="DiscordFan", message="`x`"), via_webhook=True)
|
||||
self.assertEqual(body["username"], "D1scordFan (Nimbus Station)")
|
||||
self.assertEqual(body["content"], "\\`x\\`")
|
||||
self.assertEqual(body["allowed_mentions"], {"parse": []})
|
||||
|
||||
def test_no_zone(self):
|
||||
self.assertEqual(to_discord(game(zone_id=0, zone_name=""), False)["content"], "**Bob**: hi")
|
||||
|
||||
def test_skipped_note(self):
|
||||
self.assertTrue(to_discord(game(), False, skipped=4)["content"].startswith("-# 4 message(s) skipped"))
|
||||
|
||||
def test_escape(self):
|
||||
self.assertEqual(escape_markdown("a_b|c>d[e](f)"), "a\\_b\\|c\\>d\\[e\\](f)")
|
||||
|
||||
def test_webhook_username(self):
|
||||
self.assertEqual(webhook_username("clyde"), "Clyd3")
|
||||
self.assertEqual(webhook_username(" "), "Player")
|
||||
self.assertEqual(len(webhook_username("x" * 200)), 80)
|
||||
|
||||
|
||||
class FromDiscord(unittest.TestCase):
|
||||
def event(self, **kw):
|
||||
base = {"type": 0, "channel_id": "55", "content": "hello", "author": {"id": "1", "username": "alice", "global_name": "Alice"},
|
||||
"member": {"nick": None}, "mentions": [], "attachments": []}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
def test_display_name(self):
|
||||
self.assertEqual(display_name(self.event()), "Alice")
|
||||
self.assertEqual(display_name(self.event(member={"nick": "Ally"})), "Ally")
|
||||
self.assertEqual(display_name(self.event(author={"id": "1", "username": "alice"})), "alice")
|
||||
|
||||
def test_text(self):
|
||||
e = self.event(content="hi <@2> and <@!3> in <#9> <:lego:123> <@&4> <t:1700000000:R>",
|
||||
mentions=[{"id": "2", "username": "bob"}, {"id": "3", "username": "c", "member": {"nick": "Cee"}}],
|
||||
attachments=[{"id": "x"}])
|
||||
self.assertEqual(from_discord(e), "hi @bob and @Cee in #channel :lego: @role (a time) [attachment]")
|
||||
|
||||
def test_forward_rules(self):
|
||||
self.assertTrue(should_forward(self.event(), "55", "999"))
|
||||
self.assertFalse(should_forward(self.event(channel_id="56"), "55", "999")) # another channel
|
||||
self.assertFalse(should_forward(self.event(webhook_id="7"), "55", "999")) # our own webhook posts
|
||||
self.assertFalse(should_forward(self.event(author={"id": "8", "bot": True}), "55", "999"))
|
||||
self.assertFalse(should_forward(self.event(author={"id": "999"}), "55", "999")) # the bot itself
|
||||
self.assertFalse(should_forward(self.event(type=7), "55", "999")) # a join notice
|
||||
self.assertTrue(should_forward(self.event(type=19), "55", "999")) # a reply
|
||||
|
||||
def test_config_validation(self):
|
||||
with self.assertRaises(ValueError):
|
||||
DiscordAdapter({})
|
||||
DiscordAdapter({"webhook_url": "https://example.invalid/hook"})
|
||||
DiscordAdapter({"bot_token": "t", "channel_id": "1"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user