Skip to content

Commit

Permalink
Rework motd field, and upgrade parser
Browse files Browse the repository at this point in the history
This is the base, and draft. Just would like to hear feedback
  • Loading branch information
PerchunPak committed Jun 25, 2022
1 parent 223d6f2 commit 43db85e
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 142 deletions.
4 changes: 2 additions & 2 deletions mcstatus/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def status():
player_sample = "No players online"

click.echo(f"version: v{response.version.name} (protocol {response.version.protocol})")
click.echo(f'description: "{response.description}"')
click.echo(f'description: "{response.motd.plain}"')
click.echo(f"players: {response.players.online}/{response.players.max} {player_sample}")


Expand All @@ -88,7 +88,7 @@ def json():
status_res = server.status(tries=1)
data["version"] = status_res.version.name
data["protocol"] = status_res.version.protocol
data["motd"] = status_res.description
data["motd"] = status_res.motd.plain
data["player_count"] = status_res.players.online
data["player_max"] = status_res.players.max
data["players"] = []
Expand Down
3 changes: 2 additions & 1 deletion mcstatus/bedrock_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import asyncio_dgram

from mcstatus.address import Address
from mcstatus.motd import Motd


class BedrockServerStatus:
Expand Down Expand Up @@ -96,6 +97,6 @@ def __init__(
self.latency = latency
self.players_online = players_online
self.players_max = players_max
self.motd = motd
self.motd = Motd.parse(motd)
self.map = map_
self.gamemode = gamemode
34 changes: 21 additions & 13 deletions mcstatus/motd.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
from enum import Enum
from typing import List, TYPE_CHECKING, Tuple, Union

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from typing_extensions import Self

__all__ = [
"Motd",
"Formatting",
"MinecraftColor",
"RawHTMLColor",
"RawMotd",
]

MOTD_COLORS_RE = re.compile(r"([\xA7|&][0-9A-FK-OR])", re.IGNORECASE)


Expand Down Expand Up @@ -117,6 +125,7 @@ def parse(cls, raw: Union[dict, list, str], *, bedrock=False) -> Self:
reset_after = False

if entry.get("color"):
reset_after = True
try:
raw_motd.append(MinecraftColor[entry["color"].upper()])
except KeyError:
Expand All @@ -125,7 +134,6 @@ def parse(cls, raw: Union[dict, list, str], *, bedrock=False) -> Self:
for style_key, style_val in Formatting.__members__.items():
if entry.get(style_key.lower()):
raw_motd.append(style_val)
reset_after = True

raw_motd.append(entry.get("text", ""))

Expand All @@ -147,20 +155,19 @@ def _parse_as_str(cls, raw: str, *, bedrock=False) -> Self:
raw_motd: RawMotd = []

splited_raw: List[str] = MOTD_COLORS_RE.split(raw)
if splited_raw:
for element in splited_raw:
clean_element = element.lstrip("&§")
if clean_element == "g" and not bedrock:
raw_motd.append(element) # minecoin_gold on java server
continue
for element in splited_raw:
clean_element = element.lstrip("&§")
if clean_element == "g" and not bedrock:
raw_motd.append(element) # minecoin_gold on java server
continue
try:
raw_motd.append(MinecraftColor(clean_element))
except ValueError:
try:
raw_motd.append(Formatting(clean_element))
except ValueError:
try:
raw_motd.append(MinecraftColor(clean_element))
except ValueError:
# just a text
raw_motd.append(element)
# just a text
raw_motd.append(element)

# `for` loop for removing empty values
return cls([e for e in raw_motd if e], bedrock)
Expand Down Expand Up @@ -207,6 +214,7 @@ def html(self) -> str:
elif isinstance(element, Formatting):
if element == Formatting.RESET:
result += on_reset
on_reset = ""
elif element == Formatting.OBFUSCATED:
result += "<span class=obfuscated>"
on_reset += "</span>"
Expand Down
41 changes: 10 additions & 31 deletions mcstatus/pinger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import datetime
import json
import random
from typing import List, Optional, Union
from typing import List, Optional

from mcstatus.address import Address
from mcstatus.motd import Motd
from mcstatus.protocol.connection import Connection, TCPAsyncSocketConnection, TCPSocketConnection
from mcstatus.utils import deprecated

STYLE_MAP = {
"bold": "l",
Expand Down Expand Up @@ -220,10 +222,15 @@ def __init__(self, raw):

players: Players
version: Version
description: str
motd: Motd
favicon: Optional[str]
latency: float = 0

@property
@deprecated(replacement="motd field")
def description(self) -> str:
return self.motd.plain

def __init__(self, raw):
self.raw = raw

Expand All @@ -237,34 +244,6 @@ def __init__(self, raw):

if "description" not in raw:
raise ValueError("Invalid status object (no 'description' value)")
self.description = self._parse_description(raw["description"])
self.motd = Motd.parse(raw["description"])

self.favicon = raw.get("favicon")

@staticmethod
def _parse_description(raw_description: Union[dict, list, str]) -> str:
if isinstance(raw_description, str):
return raw_description

if isinstance(raw_description, dict):
entries = raw_description.get("extra", [])
end = raw_description["text"]
else:
entries = raw_description
end = ""

description = ""

for entry in entries:
for style_key, style_val in STYLE_MAP.items():
if entry.get(style_key):
try:
if isinstance(style_val, dict):
style_val = style_val[entry[style_key]]

description += f"§{style_val}"
except KeyError:
pass # ignoring these key errors strips out html color codes
description += entry.get("text", "")

return description + end
212 changes: 212 additions & 0 deletions mcstatus/tests/test_motd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import pytest

from mcstatus.motd import Formatting, MinecraftColor, Motd, RawHTMLColor


class TestRawHTMLColor:
@pytest.mark.parametrize(
"hex,rgb",
[
("#bfff00", (191, 255, 0)),
("#00ff80", (0, 255, 128)),
("#4000ff", (64, 0, 255)),
],
)
def test_hex_to_rgb_correct(self, hex, rgb):
assert RawHTMLColor(hex).rgb == rgb

def test_hex_in_output_has_number_sign(self):
assert RawHTMLColor("#bfff00").hex.startswith("#")

def test_fail_on_incorrect_hex(self):
with pytest.raises(ValueError):
RawHTMLColor("abcd")


class TestMotdParse:
@pytest.fixture
def source(self):
return {
"extra": [
{"text": " "},
{"strikethrough": True, "color": "#b3eeff", "text": "="},
{"strikethrough": True, "color": "#b9ecff", "text": "="},
{"strikethrough": True, "color": "#c0eaff", "text": "="},
{"strikethrough": True, "color": "#c7e8ff", "text": "="},
{"strikethrough": True, "color": "#cee6ff", "text": "="},
{"strikethrough": True, "color": "#d5e4ff", "text": "="},
{"strikethrough": True, "color": "#dce2ff", "text": "="},
{"strikethrough": True, "color": "#e3e0ff", "text": "="},
{"strikethrough": True, "color": "#eadeff", "text": "="},
{"strikethrough": True, "color": "#f1dcff", "text": "="},
{"strikethrough": True, "color": "#f8daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"strikethrough": True, "color": "#f4dcff", "text": "="},
{"strikethrough": True, "color": "#f9daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"color": "white", "text": " "},
{"bold": True, "color": "#66ff99", "text": "C"},
{"bold": True, "color": "#75f5a2", "text": "r"},
{"bold": True, "color": "#84ebab", "text": "e"},
{"bold": True, "color": "#93e2b4", "text": "a"},
{"bold": True, "color": "#a3d8bd", "text": "t"},
{"bold": True, "color": "#b2cfc6", "text": "i"},
{"bold": True, "color": "#c1c5cf", "text": "v"},
{"bold": True, "color": "#d1bbd8", "text": "e"},
{"bold": True, "color": "#e0b2e1", "text": "F"},
{"bold": True, "color": "#efa8ea", "text": "u"},
{"bold": True, "color": "#ff9ff4", "text": "n "},
{"strikethrough": True, "color": "#b3eeff", "text": "="},
{"strikethrough": True, "color": "#b9ecff", "text": "="},
{"strikethrough": True, "color": "#c0eaff", "text": "="},
{"strikethrough": True, "color": "#c7e8ff", "text": "="},
{"strikethrough": True, "color": "#cee6ff", "text": "="},
{"strikethrough": True, "color": "#d5e4ff", "text": "="},
{"strikethrough": True, "color": "#dce2ff", "text": "="},
{"strikethrough": True, "color": "#e3e0ff", "text": "="},
{"strikethrough": True, "color": "#eadeff", "text": "="},
{"strikethrough": True, "color": "#f1dcff", "text": "="},
{"strikethrough": True, "color": "#f8daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"strikethrough": True, "color": "#f4dcff", "text": "="},
{"strikethrough": True, "color": "#f9daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"color": "white", "text": " \n "},
{"bold": True, "color": "#E5E5E5", "text": "The server has been updated to "},
{"bold": True, "color": "#97ABFF", "text": "1.17.1"},
],
"text": "",
}

def test_correct_result(self, source):
assert Motd.parse(source) == Motd(
[
# fmt: off
" ",
RawHTMLColor(hex="#b3eeff", rgb=(179, 238, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#b9ecff", rgb=(185, 236, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#c0eaff", rgb=(192, 234, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#c7e8ff", rgb=(199, 232, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#cee6ff", rgb=(206, 230, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#d5e4ff", rgb=(213, 228, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#dce2ff", rgb=(220, 226, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#e3e0ff", rgb=(227, 224, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#eadeff", rgb=(234, 222, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f1dcff", rgb=(241, 220, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f8daff", rgb=(248, 218, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#ffd9ff", rgb=(255, 217, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f4dcff", rgb=(244, 220, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f9daff", rgb=(249, 218, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#ffd9ff", rgb=(255, 217, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
MinecraftColor.WHITE, " ", Formatting.RESET,
RawHTMLColor(hex="#66ff99", rgb=(102, 255, 153)), Formatting.BOLD, "C", Formatting.RESET,
RawHTMLColor(hex="#75f5a2", rgb=(117, 245, 162)), Formatting.BOLD, "r", Formatting.RESET,
RawHTMLColor(hex="#84ebab", rgb=(132, 235, 171)), Formatting.BOLD, "e", Formatting.RESET,
RawHTMLColor(hex="#93e2b4", rgb=(147, 226, 180)), Formatting.BOLD, "a", Formatting.RESET,
RawHTMLColor(hex="#a3d8bd", rgb=(163, 216, 189)), Formatting.BOLD, "t", Formatting.RESET,
RawHTMLColor(hex="#b2cfc6", rgb=(178, 207, 198)), Formatting.BOLD, "i", Formatting.RESET,
RawHTMLColor(hex="#c1c5cf", rgb=(193, 197, 207)), Formatting.BOLD, "v", Formatting.RESET,
RawHTMLColor(hex="#d1bbd8", rgb=(209, 187, 216)), Formatting.BOLD, "e", Formatting.RESET,
RawHTMLColor(hex="#e0b2e1", rgb=(224, 178, 225)), Formatting.BOLD, "F", Formatting.RESET,
RawHTMLColor(hex="#efa8ea", rgb=(239, 168, 234)), Formatting.BOLD, "u", Formatting.RESET,
RawHTMLColor(hex="#ff9ff4", rgb=(255, 159, 244)), Formatting.BOLD, "n ", Formatting.RESET,
RawHTMLColor(hex="#b3eeff", rgb=(179, 238, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#b9ecff", rgb=(185, 236, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#c0eaff", rgb=(192, 234, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#c7e8ff", rgb=(199, 232, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#cee6ff", rgb=(206, 230, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#d5e4ff", rgb=(213, 228, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#dce2ff", rgb=(220, 226, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#e3e0ff", rgb=(227, 224, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#eadeff", rgb=(234, 222, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f1dcff", rgb=(241, 220, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f8daff", rgb=(248, 218, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#ffd9ff", rgb=(255, 217, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f4dcff", rgb=(244, 220, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#f9daff", rgb=(249, 218, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
RawHTMLColor(hex="#ffd9ff", rgb=(255, 217, 255)), Formatting.STRIKETHROUGH, "=", Formatting.RESET,
MinecraftColor.WHITE, " \n ", Formatting.RESET,
RawHTMLColor(hex="#E5E5E5", rgb=(229, 229, 229)), Formatting.BOLD,
"The server has been updated to ", Formatting.RESET,
RawHTMLColor(hex="#97ABFF", rgb=(151, 171, 255)), Formatting.BOLD, "1.17.1", Formatting.RESET,
"",
# fmt: on
]
)

@pytest.mark.parametrize("bedrock", (True, False))
def test_bedrock_parameter_nothing_changes(self, bedrock: bool):
assert Motd.parse([{"color": "minecoin_gold", "text": " "}], bedrock=bedrock).raw == [
MinecraftColor.MINECOIN_GOLD,
" ",
Formatting.RESET,
"",
]

@pytest.mark.parametrize("bedrock", (True, False))
def test_parse_as_str_ignore_minecoin_gold_on_java(self, bedrock: bool):
assert Motd.parse("&g", bedrock=bedrock).raw == [MinecraftColor.MINECOIN_GOLD if bedrock else "&g"]

@pytest.mark.parametrize("input", ["", [], {"extra": [], "text": ""}])
def test_empty_input_also_empty_raw(self, input):
assert Motd.parse(input).raw == [] if isinstance(input, str) else [""]

@pytest.mark.parametrize("plain_motd", ["&1&2&3", "§123§5bc", "§1§2§3"])
def test_plain_return_the_same(self, plain_motd: str):
assert Motd.parse(plain_motd).plain == plain_motd.replace("&", "§")

def test_plain_skip_html_colors(self):
assert Motd.parse({"extra": [{"color": "#4000ff", "text": "colored text"}], "text": ""}).plain == "colored text§r"

@pytest.mark.parametrize("bedrock", (True, False))
def test_html_return_correct(self, bedrock: bool):
assert Motd.parse(
{
"extra": [
{"text": "1"},
{"color": "#b3eeff", "text": "2"},
{"obfuscated": True, "color": "black", "text": "3"},
{"bold": True, "strikethrough": True, "color": "dark_blue", "text": "4"},
{"italic": True, "color": "dark_green", "text": "5"},
{"underlined": True, "color": "dark_aqua", "text": "6"},
{"color": "dark_aqua", "text": "7"},
{"color": "dark_red", "text": "8"},
{"color": "dark_purple", "text": "9"},
{"color": "gold", "text": "10"},
{"color": "gray", "text": "11"},
{"color": "dark_gray", "text": "12"},
{"color": "blue", "text": "13"},
{"color": "green", "text": "14"},
{"color": "aqua", "text": "15"},
{"color": "red", "text": "16"},
{"color": "light_purple", "text": "17"},
{"color": "yellow", "text": "18"},
{"color": "white", "text": "19"},
{"color": "minecoin_gold", "text": "20"},
],
"text": "",
},
bedrock=bedrock,
).html == (
"<p>1"
"<span style='color:#b3eeff'>2</span>"
"<span style='color:#000000;text-shadow:0 0 1px #000000'><span class=obfuscated>3</span></span>"
"<span style='color:#0000AA;text-shadow:0 0 1px #00002A'><b><s>4</span></b></s>"
"<span style='color:#00AA00;text-shadow:0 0 1px #002A00'><i>5</span></i>"
"<span style='color:#00AAAA;text-shadow:0 0 1px #002A2A'><u>6</span></u>"
"<span style='color:#00AAAA;text-shadow:0 0 1px #002A2A'>7</span>"
"<span style='color:#AA0000;text-shadow:0 0 1px #AA0000'>8</span>"
"<span style='color:#AA00AA;text-shadow:0 0 1px #2A002A'>9</span>"
"<span style='color:#FFAA00;text-shadow:0 0 1px #" + ("402A00" if bedrock else "2A2A00") + "'>10</span>"
"<span style='color:#AAAAAA;text-shadow:0 0 1px #2A2A2A'>11</span>"
"<span style='color:#555555;text-shadow:0 0 1px #151515'>12</span>"
"<span style='color:#5555FF;text-shadow:0 0 1px #15153F'>13</span>"
"<span style='color:#55FF55;text-shadow:0 0 1px #153F15'>14</span>"
"<span style='color:#55FFFF;text-shadow:0 0 1px #153F3F'>15</span>"
"<span style='color:#FF5555;text-shadow:0 0 1px #3F1515'>16</span>"
"<span style='color:#FF55FF;text-shadow:0 0 1px #3F153F'>17</span>"
"<span style='color:#FFFF55;text-shadow:0 0 1px #3F3F15'>18</span>"
"<span style='color:#FFFFFF;text-shadow:0 0 1px #3F3F3F'>19</span>"
"<span style='color:#DDD605;text-shadow:0 0 1px #373501'>20</span>"
"</p>"
)
Loading

0 comments on commit 43db85e

Please sign in to comment.