Unverified Commit 9910c787 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(limbo): 登录门聊天和 /link 回复里的网址可以直接点击打开

parent 1e42e9a8
Loading
Loading
Loading
Loading
+1 −1
Changes for plugins/limbo/src/main/java/best/lolicon/felis/limbo/FelisLimboPlugin.java: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -230,7 +230,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {

        @Override
        public void chat(String line) {
            player.sendMessage(line);
            player.sendMessage(LoginBook.chatLine(line));
        }

        @Override
+22 −3
Changes for plugins/limbo/src/main/java/best/lolicon/felis/limbo/LoginBook.java: 22 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -4,14 +4,28 @@ import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;

import java.util.regex.Pattern;

/**
 * LoginBook is the book the gate opens on join: the bind code, a line that opens the
 * login page when clicked, and the reminder to use the system browser, where
 * WebAuthn/passkey works (the WeChat/QQ in-app browsers break it).
 * LoginBook renders what the gate shows a player: the book it opens on join (the bind
 * code, a line that opens the login page when clicked, and the reminder to use the
 * system browser, where WebAuthn/passkey works; the WeChat/QQ in-app browsers break
 * it), and the chat lines, whose web addresses open when clicked.
 */
final class LoginBook {

    // A web address in a chat line runs to the next space. The client makes nothing
    // in server chat clickable by itself, so a player who closed the book had to type
    // the login URL out by hand.
    private static final LegacyComponentSerializer CHAT = LegacyComponentSerializer.builder()
            .character(LegacyComponentSerializer.SECTION_CHAR)
            .extractUrls(Pattern.compile("https?://\\S+"), Style.style(NamedTextColor.AQUA, TextDecoration.UNDERLINED))
            .build();

    private LoginBook() {
    }

@@ -26,4 +40,9 @@ final class LoginBook {
                        .color(NamedTextColor.GRAY));
        return Book.book(Component.text("Felis Login"), Component.text("Felis"), page);
    }

    /** chatLine renders one chat line with § colour codes, its web addresses clickable. */
    static Component chatLine(String line) {
        return CHAT.deserialize(line);
    }
}
+1 −1
Changes for plugins/limbo/src/main/java/best/lolicon/felis/limbo/LoginFlow.java: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ final class LoginFlow {
    interface Seat {
        String name();

        /** chat sends one line, with § colour codes. */
        /** chat sends one line, with § colour codes; a web address in it opens when clicked. */
        void chat(String line);

        /** showCode opens the login book. It may throw for a client that refuses it. */
+30 −0
Changes for plugins/limbo/test/best/lolicon/felis/limbo/LoginFlowTest.java: 30 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.TextDecoration;

import java.io.IOException;
import java.io.OutputStream;
@@ -119,6 +120,13 @@ public final class LoginFlowTest {
                List.of(CODE + " " + stub.panelUrl), seat.books);
        assertTrue("chat carries the code", seat.chat.get(0).contains(CODE));
        assertTrue("chat names the panel URL", seat.chat.get(1).contains(stub.panelUrl) && seat.chat.get(2).contains(stub.panelUrl));
        // The client makes nothing in server chat clickable by itself: a player who
        // closed the book had to type the URL out.
        for (int i = 1; i <= 2; i++) {
            List<ClickEvent> clicks = new ArrayList<>();
            collectClicks(LoginBook.chatLine(seat.chat.get(i)), clicks);
            assertEq("chat line " + i + " opens the panel URL", List.of(ClickEvent.openUrl(stub.panelUrl)), clicks);
        }
        assertEq("no release before the link", 0, seat.releases.size());

        int before = stub.hits("/link/status/");
@@ -357,6 +365,19 @@ public final class LoginFlowTest {
        collectClicks(page, clicks);
        assertEq("one click, opening the login page", List.of(ClickEvent.openUrl(CONSOLE + "/link")), clicks);
        assertTrue("the page says to use the system browser", plain(page).contains("SYSTEM browser"));

        // A chat line keeps its text; only the address in it turns into a link.
        Component line = LoginBook.chatLine("§e[Felis] 打开 §b" + CONSOLE + "/link §e完成登录(勿用微信/QQ内置浏览器)。");
        assertEq("chat line text", "[Felis] 打开 " + CONSOLE + "/link 完成登录(勿用微信/QQ内置浏览器)。", plain(line));
        List<Component> links = new ArrayList<>();
        collectLinks(line, links);
        assertEq("one link in the line", 1, links.size());
        assertEq("the link is the address alone", CONSOLE + "/link", plain(links.get(0)));
        assertEq("the link opens it", ClickEvent.openUrl(CONSOLE + "/link"), links.get(0).clickEvent());
        assertEq("the link looks like one", TextDecoration.State.TRUE, links.get(0).decoration(TextDecoration.UNDERLINED));
        List<ClickEvent> none = new ArrayList<>();
        collectClicks(LoginBook.chatLine(LoginFlow.SIGNED_IN), none);
        assertEq("a line without an address has no link", List.of(), none);
    }

    // ---- fakes ----
@@ -642,6 +663,15 @@ public final class LoginFlowTest {
        return sb.toString();
    }

    private static void collectLinks(Component c, List<Component> out) {
        if (c.clickEvent() != null) {
            out.add(c);
        }
        for (Component child : c.children()) {
            collectLinks(child, out);
        }
    }

    private static void collectClicks(Component c, List<ClickEvent> out) {
        if (c.clickEvent() != null) {
            out.add(c.clickEvent());
+11 −2
Changes for plugins/test.sh: 11 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -26,8 +26,9 @@
#      prompt, the op-login approval card names the account and leaves its
#      name for the admin to type, and the loader mods' shared /link refuses the
#      console and an offline-mode server before minting, fetches the code off the
#      server thread and answers on it, and turns any failure into one line.
#      InviteCardTest and OpApprovalCardTest need the
#      server thread and answers on it, and turns any failure into one line; the
#      /link reply makes the panel address a link the player can click open.
#      InviteCardTest, OpApprovalCardTest and LinkCodeCardTest need the
#      adventure jars the velocity plugin compiles
#      against; they are fetched from Maven Central below, pinned by version and
#      checked by digest (a test run against silently-substituted bytes is not a
@@ -213,6 +214,14 @@ javac -cp "$adventure_api:$adventure_key:$examination_api" -d "$work/opcard-clas
java -cp "$work/opcard-classes:$adventure_api:$adventure_key:$examination_api" \
  best.lolicon.felis.velocity.OpApprovalCardTest

echo "==> LinkCodeCardTest (/link reply, velocity)"
mkdir -p "$work/linkcard-classes"
javac -cp "$adventure_api:$adventure_key:$examination_api" -d "$work/linkcard-classes" \
  plugins/velocity/src/main/java/best/lolicon/felis/velocity/LinkCodeCard.java \
  plugins/velocity/test/best/lolicon/felis/velocity/LinkCodeCardTest.java
java -cp "$work/linkcard-classes:$adventure_api:$adventure_key:$examination_api" \
  best.lolicon.felis.velocity.LinkCodeCardTest

# --- 2. production compile gates ------------------------------------------------

for module in velocity paper; do
Loading