Open2ch 強制コテ化スクリプト

IDをもとにアイコンを自動生成

// ==UserScript==
// @name         Open2ch 強制コテ化スクリプト
// @namespace    https://onjmin.glitch.me/
// @version      1.0.1
// @description  IDをもとにアイコンを自動生成
// @author       おんJ民
// @match        *://*.open2ch.net/*/*
// @icon         https://avatars.githubusercontent.com/u/88383494
// @grant        none
// @license      GNU Affero General Public License v3.0 or later
// ==/UserScript==

(async ()=> {
    'use strict';

    const currentKey = 'robohash';
    // const currentKey = 'dicebear/big-smile';
    const avatarModes = [
        {
            key: "robohash",
            url: (hash) => `https://robohash.org/${hash}.png`,
            flip: true,
        },
        {
            key: "dicebear",
            url: (hash) =>
            `https://api.dicebear.com/8.x/adventurer/svg?seed=${hash}`,
            flip: true,
        },
        {
            key: "gravatar",
            url: (hash) => `https://www.gravatar.com/avatar/${hash}?d=identicon`,
            flip: false,
        },
    ];

    const dicebearList = [
        "adventurer", // 冒険者風キャラ(RPGっぽい)
        "avataaars", // Avataaarsベースのカートゥーン
        "big-ears", // 大きな耳が特徴のスタイル
        "big-smile", // 巨大スマイル系アバター
        "bottts", // ボット風ロボットアバター
        "croodles", // ラフな手描きキャラ
        "identicon", // GitHub風識別子
        "initials", // 名前のイニシャルを表示
        "lorelei", // 魔法使いや精霊風の人物像
        "miniavs", // シンプルなミニアバター
        "notionists", // Notion風人物
        "open-peeps", // カジュアルな人物画(肩まで)
        "personas", // ややリアル寄りの人物
        "pixel-art", // ドット絵スタイル
    ];

    const dicebear = avatarModes.find(({key}) => key === "dicebear");
    for (const v of dicebearList) {
        avatarModes.push({
            ...dicebear,
            key: `dicebear/${v}`,
            url: (hash) => `https://api.dicebear.com/8.x/${v}/svg?seed=${hash}`,
            flip: v.includes('adventurer')
        });
    }

    // ✅ アバター要素生成
    const createAvatar = (url, flip) => {
        const isSVG = url.endsWith(".svg");
        const img = document.createElement("img");
        Object.assign(img, {
            src: url,
            width: 45,
            height: 45,
        });
        Object.assign(img.style, {
            padding: "3px",
            background: isSVG ? "transparent" : "rgb(238, 238, 255)",
            marginRight: "3px",
            borderRadius: "45px",
            display: "block",
            transform: flip ? "scaleX(-1)" : "",
        });
        img.setAttribute("iconimg", "1");
        img.setAttribute("align", "left");
        img.classList.add("pic", "lazy", "imgur");

        const a = document.createElement("a");
        a.href = url;
        a.target = "_blank";
        a.setAttribute("data-lightbox", "i");
        a.appendChild(img);

        return a;
    };

    const extractID = (dt) => {
        const span = dt.querySelector("span._id");
        if (span?.getAttribute("val")) return span.getAttribute("val");
        const match = [...dt.classList].find((cls) => cls.startsWith("id"));
        return match?.slice(2).trim();
    };

    const sha256 = async text =>
    Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text))))
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");

    const processPosts = async (nodes) => {
        for (const node of nodes) {
            if (!(node instanceof HTMLElement)) continue;
            const dts = node.querySelectorAll?.("dt[class*='id']") ?? [];
            for (const dt of dts) {
                const id = extractID(dt);
                if (!id) continue;

                const dd = dt.nextElementSibling;
                if (!dd?.matches("dd")) continue;

                // 削除既存アバター
                dd.querySelectorAll('img[iconimg="1"]').forEach((img) => {
                    const a = img.closest("a");
                    a && a.parentElement === dd ? a.remove() : img.remove();
                });

                const mode = avatarModes.find((m) => m.key === currentKey);
                if (!mode) return;

                let hash = '';
                if (/[^0-9a-zA-Z]/.test(id)) {
                    const str = id.replace(/[^0-9a-zA-Z]/g, '');
                    if (str.length) {
                        hash = str;
                    } else {
                        hash = (await sha256(str)).slice(32);
                    }
                } else {
                    hash = id;
                }

                const avatarURL = mode.url(hash);
                const avatar = createAvatar(avatarURL, mode.flip);

                const icon = dd.querySelector("icon");
                icon
                    ? dd.insertBefore(avatar, icon)
                : dd.insertBefore(avatar, dd.firstChild);
            }
        }
    };

    const thread = document.querySelector(".thread");
    if (thread) {
        // 初回に既存要素も処理
        await processPosts(thread.children);

        // 監視設定
        const observer = new MutationObserver((mutations) => {
            const added = mutations.flatMap((m) => [...m.addedNodes]);
            processPosts(added);
        });

        observer.observe(thread, { childList: true, subtree: true });
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址