「5chどんぐり対応を効率化するFirefoxアドオンの作り方とコード全公開」

はじめに

5ちゃんねるを利用する中で、特にどんぐりシステムが導入された板では、投稿を効率的に管理する必要があります。どんぐりシステムは、掲示板のセキュリティを強化するために導入された仕組みですが、時にはユーザーの利便性を損なうこともあります。そこで、どんぐり対応を効率化するためのFirefoxアドオンを開発しました。本記事では、アドオンの作り方を解説し、実際に使用するコードをすべて公開します。
※この記事は以前ブログであげた内容の再掲になります。


どんぐりシステムとは?

どんぐりシステムは、5ちゃんねる内で特定の掲示板において、ユーザーの投稿を管理するシステムです。このシステムは、SETTING.TXTという設定ファイル内の「bbs_Acorn」という値が1か2に設定されている板で主に稼働します。最近では、VIPQ2の値が2以上になっている掲示板でも手動でどんぐりシステムを導入することが可能です。

どんぐりシステムが導入されると、初めてアクセスするブラウザでの書き込みが制限されることがあります。システムがレベルを監視し、レベルが一定基準を下回ると書き込みができなくなる仕組みです。また、一部の掲示板では「大砲禁止コマンド」を使用することで、特定のユーザーによる過剰な干渉を防ぐことが可能です。


Firefoxアドオンの作り方

1. 必要なファイルの準備

Firefoxアドオンを作成するには、以下の3つの主要なファイルが必要です。

  1. manifest.json: アドオンの基本情報や権限、設定を定義します。

  2. sidebar.html: サイドバーに表示されるユーザーインターフェースを定義します。

  3. sidebar.js: アドオンの機能を実装するJavaScriptコードです。

2. 各ファイルのコード

2.1 manifest.json

manifest.jsonは、アドオンの基本情報を定義するファイルです。ここでは、アドオンの名前、バージョン、アイコン、権限などを設定します。

{
  "manifest_version": 3,
  "name": "5ch専用ブラウザアドオン",
  "version": "1.0",
  "description": "5chのスレッドを取得し、表示・保存するためのブラウザアドオンです。",
  "permissions": [
    "tabs",
    "activeTab",
    "storage",
    "downloads"
  ],
  "sidebar_action": {
    "default_panel": "sidebar.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    },
    "default_title": "5ch専用ブラウザ"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "host_permissions": [
    "https://menu.5ch.net/*",
    "https://*.5ch.net/*"
  ]
}

2.2 sidebar.html

sidebar.htmlは、サイドバーに表示されるUI(ユーザーインターフェース)を定義します。HTMLとCSSで構成されており、ユーザーが操作するためのフォームやボタンが含まれています。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>5ch Browser</title>
  <style>
    body {
      font-family: 'Arial', sans-serif;
      font-size: 12px;
      width: 400px;
      margin: 0;
      padding: 10px;
    }
    select, input, button {
      font-size: 12px;
      margin-bottom: 10px;
      width: 100%;
    }
    table {
      width: 100%;
      border-collapse: collapse;
    }
    th, td {
      border: 1px solid black;
      padding: 5px;
      text-align: left;
    }
    #post-container {
      margin-top: 20px;
    }
    #setting-content {
      font-size: 6px;
      color: #ddd;
      line-height: 1.2;
    }
  </style>
</head>
<body>
  <h1>5ch Browser</h1>

  <select id="board-select">
    <option value="">板を選択...</option>
  </select>
  <button id="fetch-threads">スレッドを取得</button>
  <button id="refresh-all-dat">全datを更新</button>

  <input type="text" id="search-term" placeholder="検索キーワードを入力" />
  <button id="search-button">検索</button>

  <h3>Donguri 検索</h3>
  <input type="text" id="donguri-part1" placeholder="1つ目のXの部分を入力" />
  <input type="text" id="donguri-part2" placeholder="2つ目のXの部分を入力 (オプション)" />
  <button id="donguri-search-button">Donguri 検索</button>

  <table>
    <thead>
      <tr>
        <th>dat番号</th>
        <th>スレッドタイトル</th>
        <th>URL</th>
        <th>特定文字列</th>
      </tr>
    </thead>
    <tbody id="thread-list">
    </tbody>
  </table>

  <pre id="setting-content"></pre>

  <script src="sidebar.js"></script>
</body>
</html>


2.3 sidebar.js

sidebar.jsは、アドオンのロジックを実装するJavaScriptファイルです。ユーザーの操作に応じてスレッドを取得したり、検索機能を提供します。

document.addEventListener('DOMContentLoaded', function() {
  const boardSelect = document.getElementById('board-select');
  const threadListElement = document.getElementById('thread-list');
  const settingContentElement = document.getElementById('setting-content');
  const searchButton = document.getElementById('search-button');
  const searchTermInput = document.getElementById('search-term');
  const donguriPart1Input = document.getElementById('donguri-part1');
  const donguriPart2Input = document.getElementById('donguri-part2');
  const donguriSearchButton = document.getElementById('donguri-search-button');
  let currentBoardUrl = '';

  // 板のリストを取得
  fetch('https://menu.5ch.net/bbstable.html')
    .then(response => response.arrayBuffer())  // ArrayBufferとして取得
    .then(buffer => {
      const decoder = new TextDecoder('Shift_JIS');  // Shift_JISでデコード
      const data = decoder.decode(buffer);
      const parser = new DOMParser();
      const doc = parser.parseFromString(data, 'text/html');
      const boards = doc.querySelectorAll('a');

      boards.forEach(board => {
        const option = document.createElement('option');
        option.value = board.href;
        option.textContent = board.textContent;
        boardSelect.appendChild(option);
      });
    })
    .catch(error => console.error('Error fetching board list:', error));

  // スレッド取得
  document.getElementById('fetch-threads').addEventListener('click', function() {
    currentBoardUrl = boardSelect.value;

    if (!currentBoardUrl) {
      alert('板を選択してください。');
      return;
    }

    fetchThreads(currentBoardUrl);
    fetchSetting(currentBoardUrl);
  });

  // 全dat更新ボタン
  document.getElementById('refresh-all-dat').addEventListener('click', function() {
    if (currentBoardUrl) {
      fetchThreads(currentBoardUrl);
    } else {
      alert('まず板を選択してください。');
    }
  });

  // 検索ボタンのクリックイベント
  searchButton.addEventListener('click', function() {
    const searchTerm = searchTermInput.value.trim().toLowerCase();
    if (!searchTerm && searchTerm !== "none") {
      alert('検索キーワードを入力してください。');
      return;
    }
    searchThreads(searchTerm);
  });

  // Donguri 検索ボタンのクリックイベント
  donguriSearchButton.addEventListener('click', function() {
    const part1 = donguriPart1Input.value.trim();
    const part2 = donguriPart2Input.value.trim();
    if (!part1) {
      alert('少なくとも1つのXの部分を入力してください。');
      return;
    }
    searchDonguri(part1, part2);
  });

  // SETTING.TXTの取得
  function fetchSetting(boardUrl) {
    const settingUrl = boardUrl.replace('/l50', '') + 'SETTING.TXT';

    fetch(settingUrl)
      .then(response => response.arrayBuffer())
      .then(buffer => {
        const decoder = new TextDecoder('Shift_JIS');
        const data = decoder.decode(buffer);
        settingContentElement.textContent = data;
      })
      .catch(error => {
        console.error('Error fetching SETTING.TXT:', error);
        settingContentElement.textContent = '設定情報は利用できません';
      });
  }

  // スレッドの取得
  function fetchThreads(boardUrl) {
    const subjectUrl = boardUrl.replace('/l50', '') + 'subject.txt';

    fetch(subjectUrl)
      .then(response => response.arrayBuffer())  // ArrayBufferとして取得
      .then(buffer => {
        const decoder = new TextDecoder('Shift_JIS');  // Shift_JISでデコード
        const data = decoder.decode(buffer);
        const threads = data.split('\n');
        threadListElement.innerHTML = ''; // 前の結果をクリア

        threads.forEach(thread => {
          const [threadInfo, threadTitleRaw] = thread.split("<>");
          const threadTitle = threadTitleRaw ? threadTitleRaw.split('(')[0].trim() : "Unknown Title";
          const match = threadInfo.match(/^(\d{10,14})\.dat/);
          const threadId = match ? match[1] : null;

          if (threadId) {
            const threadUrl = generate5chTestUrl(boardUrl, threadId);

            const listItem = document.createElement('tr');

            const datFileCell = document.createElement('td');
            datFileCell.textContent = `${threadId}.dat`;

            const titleCell = document.createElement('td');
            titleCell.textContent = threadTitle;

            const urlCell = document.createElement('td');
            const threadLink = document.createElement('a');
            threadLink.href = threadUrl;
            threadLink.textContent = threadUrl;
            threadLink.target = '_blank'; // 新しいタブで5chのページを表示
            urlCell.appendChild(threadLink);

            // DATファイルをダウンロードする
            threadLink.addEventListener('click', function(event) {
              event.preventDefault();
              downloadDatFile(threadId, boardUrl);
              window.open(threadUrl, '_blank'); // 新しいタブで5chのページを開く
            });

            // 特定の文字列を含む行を抽出
            const specialLineCell = document.createElement('td');
            fetchDatFirstLine(generateDatUrl(boardUrl, threadId)).then(lastLine => {
              specialLineCell.textContent = lastLine;
            });

            listItem.appendChild(datFileCell);
            listItem.appendChild(titleCell);
            listItem.appendChild(urlCell);
            listItem.appendChild(specialLineCell); // 特定文字列列を追加

            threadListElement.appendChild(listItem);
          }
        });
      })
      .catch(error => console.error('Error fetching thread list:', error));
  }

  // 5chのtestスレッドURLの生成
  function generate5chTestUrl(boardUrl, threadId) {
    const url = new URL(boardUrl);
    const serverName = url.hostname.split('.')[0]; // サーバー名
    const boardName = url.pathname.split('/')[1];  // 板名
    return `https://itest.5ch.net/${serverName}/test/read.cgi/${boardName}/${threadId}/`;
  }

  // DATファイルURLの生成
  function generateDatUrl(boardUrl, threadId) {
    return `${boardUrl.replace('/l50', '')}dat/${threadId}.dat`;
  }

  // DATファイルの1行目の最後のVIPQ2_EXTDAT行を取得
  function fetchDatFirstLine(url) {
    return fetch(url)
      .then(response => response.arrayBuffer())
      .then(buffer => {
        const decoder = new TextDecoder('Shift_JIS'); // Shift_JISでデコード
        const data = decoder.decode(buffer);
        const lines = data.split('\n');
        const vipq2Lines = lines.filter(line => line.includes('VIPQ2_EXTDAT:'));
        if (vipq2Lines.length > 0) {
          const lastVipq2Line = vipq2Lines[vipq2Lines.length - 1];
          const match = lastVipq2Line.match(/VIPQ2_EXTDAT:.+$/);
          return match ? match[0] : 'none';
        }
        return 'none';
      })
      .catch(error => {
        console.error('Error fetching DAT file last line:', error);
        return 'none';
      });
  }

  // 特定のdonguri文字列を検索する機能
  function searchDonguri(part1, part2) {
    const rows = threadListElement.querySelectorAll('tr');
    const regex1 = new RegExp(`donguri=.*?/${part1}: EXT was configured`);
    const regex2 = part2 ? new RegExp(`donguri=.*?/${part2}: EXT was configured`) : null;
    const taipoRegex = /大砲禁止/;

    rows.forEach(row => {
      const specialCell = row.querySelector('td:last-child');
      if (specialCell && (regex1.test(specialCell.textContent) || (regex2 && regex2.test(specialCell.textContent)))) {
        row.style.display = '';
      } else {
        row.style.display = 'none';
      }
    });
  }

  // DATファイルをダウンロード
  function downloadDatFile(threadId, boardUrl) {
    const datUrl = generateDatUrl(boardUrl, threadId);
    fetch(datUrl)
      .then(response => response.blob())
      .then(blob => {
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = `${threadId}.dat`;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      })
      .catch(error => console.error('Error downloading DAT file:', error));
  }
});



アドオンの配布と使用方法

このアドオンは一時アドオンとして利用できます。以下の手順で配布と使用が可能です。また、著者はこの質の悪いコードに関して著作権は主張しません。

  1. ZIPファイルの作成: 上記の3つのファイルを含めたフォルダをZIP形式で圧縮します。

  2. 配布方法: 作成したZIPファイルを自分のウェブサイト、ブログ、クラウドストレージなどで公開し、リンクを共有します。

  3. インストール手順:

    1. ZIPファイルをダウンロードし、解凍せずそのまま保存します。

    2. Firefoxのアドオンマネージャーを開き(about:addons)、開発者モードを有効にします。

    3. 「一時的に読み込む」ボタンをクリックし、ダウンロードしたZIPファイルを選択します。

    4. これでアドオンが読み込まれ、使用可能になります(ブラウザを再起動するとアドオンは無効になります)。


まとめ

この記事では、どんぐり対応を効率化するためのFirefoxアドオンの作り方を解説し、実際のコードを公開しました。このアドオンを利用することで、5ちゃんねるでの活動がよりスムーズになり、どんぐりシステムによる制約を最小限に抑えることができます。興味のある方はぜひ、アドオンをinstallして試してみてください。

いいなと思ったら応援しよう!