Better PTT login

Use a popup to enter credentials, make your password managers work.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Better PTT login
// @namespace    NoNameSpace
// @version      1.0.1
// @description  Use a popup to enter credentials, make your password managers work.
// @match        https://term.ptt.cc/
// @match        https://term.ptt2.cc/
// @icon         https://term.ptt.cc/assets/logo_connect.c8fa42175331bab52f24fd5e64cf69bb.png
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

let USERNAME = null;
let PASSWORD = null;

// --------------------------------------------------------
// 1. Override WebSocket before any connection
// --------------------------------------------------------
const RealWebSocket = window.WebSocket;

class FakeWebSocket extends RealWebSocket {
    constructor(url) {
        super(url);

        this.sendStr = (e) => {
            for (let t = 0; t < e.length; t += 1000) {
                const part = e.substring(t, t + 1000);
                const bytes = new Uint8Array(part.split("").map(ch => ch.charCodeAt(0)));
                this.send(bytes.buffer);
            }
        };

        this.addEventListener("open", () => {
            if (USERNAME && PASSWORD) {
                loginPTT(this);
            } else {
                console.log("[PTT] Waiting for credentials from popup...");
                // Wait for credentials to arrive
                const interval = setInterval(() => {
                    if (USERNAME && PASSWORD) {
                        clearInterval(interval);
                        loginPTT(this);
                    }
                }, 200);
            }
        });
    }
}

window.WebSocket = FakeWebSocket;

// --------------------------------------------------------
// 2. Helper to send credentials to PTT via WebSocket
// --------------------------------------------------------
function loginPTT(ws) {
    console.log("[PTT] Logging in with provided credentials");
    ws.sendStr(USERNAME + "\x0d");
    ws.sendStr(PASSWORD + "\x0d");
    setTimeout(() => {
        ws.sendStr("\x0d");
        ws.sendStr("\x08");
        ws.sendStr("\x08");
        ws.sendStr("\x0d");
        ws.sendStr("\x1a");
        ws.sendStr("\x66");
    }, 500);
}

// --------------------------------------------------------
// 3. Listen for credentials from popup
// --------------------------------------------------------
window.addEventListener("message", ev => {
    if (!ev.data || ev.data.type !== "CREDENTIALS") return;

    USERNAME = ev.data.username;
    PASSWORD = ev.data.password;

    console.log("[PTT] Received credentials:", USERNAME);
});

// --------------------------------------------------------
// 4. Open popup for user credentials
// --------------------------------------------------------
function openLoginPopup() {
    // Prevent multiple modals
    if (document.getElementById("BetterPTTDialog")) return;

    // Container div
    const container = document.createElement("div");
    container.id = "BetterPTTDialog";
    container.style.position = "fixed";
    container.style.top = "0";
    container.style.left = "0";
    container.style.width = "100vw";
    container.style.height = "100vh";
    container.style.display = "flex";
    container.style.alignItems = "center";
    container.style.justifyContent = "center";
    container.style.zIndex = "9999";
    container.style.backgroundColor = "rgba(0,0,0,0.5)";

    // Dialog element
    const dialog = document.createElement("dialog");
    dialog.style.padding = "30px";
    dialog.style.borderRadius = "12px";
    dialog.style.border = "none";
    dialog.style.backgroundColor = "#1c1c1e";
    dialog.style.color = "#d1d1d6";
    dialog.style.fontFamily = "-apple-system, BlinkMacSystemFont, sans-serif";
    dialog.style.width = "320px";
    dialog.style.boxShadow = "0 8px 20px rgba(0,0,0,0.4)";

    dialog.innerHTML = `
        <h2 style="margin-bottom:20px; text-align:center;">Better PTT Login</h2>
        <form method="dialog" id="pttLoginForm" style="display:flex; flex-direction:column;">
            <input id="u" name="username" autocomplete="username" placeholder="Username" required
                style="margin-bottom:12px;padding:10px;border-radius:8px;border:1px solid #3a3a3c;background-color:#2c2c2e;color:#d1d1d6;font-size:14px;">
            <input id="p" type="password" name="password" autocomplete="current-password" placeholder="Password" required
                style="margin-bottom:20px;padding:10px;border-radius:8px;border:1px solid #3a3a3c;background-color:#2c2c2e;color:#d1d1d6;font-size:14px;">
            <button type="submit" style="padding:10px;border-radius:8px;border:none;background-color:#0a84ff;color:#fff;font-size:16px;cursor:pointer;">Login</button>
        </form>
        <div style="text-align:center;margin-top:10px;">
            <button type="button" id="closeBtn" style="background:none;border:none;color:#d1d1d6;opacity:0.6;cursor:pointer;">✖ Close</button>
        </div>
    `;

    container.appendChild(dialog);
    document.body.appendChild(container);

    // Username input
    const u = dialog.querySelector("#u");
    const p = dialog.querySelector("#p");

    // Stop key events from propagating to page
    ['keydown', 'keypress', 'keyup'].forEach(eventName => {
        container.addEventListener(eventName, e => e.stopPropagation());
    });

    // Submit form
    const form = dialog.querySelector("#pttLoginForm");
    form.addEventListener("submit", e => {
        e.preventDefault();

        if (u.value && p.value) {
            window.postMessage({
                type: "CREDENTIALS",
                username: u.value,
                password: p.value
            }, "*");
            closeModal();
        }
    });

    // ESC key closes modal
    document.addEventListener("keydown", function escHandler(e) {
        if (e.key === "Escape") closeModal();
    });

    // Close button
    dialog.querySelector("#closeBtn").addEventListener("click", closeModal);

    // Show modal
    dialog.showModal();

    // Close function
    function closeModal() {
        if (!container.parentNode) return; // already removed
        dialog.close();
        container.remove();
        dialog.removeEventListener("cancel", cancelHandler);
    }

    // Handle Esc key reliably
    function cancelHandler(e) {
        e.preventDefault(); // prevent default <dialog> Esc behavior
        closeModal();
    }

    // Listen for Esc on the dialog itself
    dialog.addEventListener("cancel", cancelHandler);
}

// --------------------------------------------------------
// 5. Trigger popup on DOMContentLoaded
// --------------------------------------------------------
window.addEventListener("DOMContentLoaded", () => {
    openLoginPopup();
});

// --------------------------------------------------------
// 6. Prevent PTT's beforeunload handler
// --------------------------------------------------------
const origAEL = window.addEventListener;
window.addEventListener = function (...args) {
    if (args[0] === "beforeunload") return;
    return origAEL.apply(this, args);
};