Greasy Fork

Greasy Fork is available in English.

5ch.net donguri Hit Response Getter

Fetches and filters hit responses from donguri 5ch boards

当前为 2024-06-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        5ch.net donguri Hit Response Getter
// @namespace   http://greasyfork.icu/users/1310758
// @description Fetches and filters hit responses from donguri 5ch boards
// @match       https://donguri.5ch.net/cannonlogs
// @connect     5ch.net
// @license     MIT License
// @author      pachimonta
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @version     2024-06-04_001
// ==/UserScript==
(function() {
  'use strict';

  // Storage for bbs list and subject list
  const bbsList = {};
  const subjectList = {};
  const completedURL = {};
  const column = ['order', 'term', 'date', 'bbs', 'bbsname', 'key', 'id', 'hunter', 'target', 'subject'];

  // Helper functions
  const $ = (selector, context = document) => context.querySelector(selector);
  const $$ = (selector, context = document) => [...context.querySelectorAll(selector)];

  const addWeekdayToDatetime = (datetimeStr) => {
    const firstColonIndex = datetimeStr.indexOf(':');

    const splitIndex = firstColonIndex - 2;
    const datePart = datetimeStr.slice(0, splitIndex);
    const timePart = datetimeStr.slice(splitIndex);

    const [year, month, day] = datePart.split('/').map(Number);

    const date = new Date(year, month - 1, day);

    const weekdays = ['日', '月', '火', '水', '木', '金', '土'];

    const weekday = weekdays[date.getDay()];

    return `${datePart}(${weekday}) ${timePart}`;
  };

  const appendEl = (elem, txt = null, elementName = 'td') => {
    if (elem.parentElement.tagName === 'THEAD') {
      elementName = 'th';
    }
    const e = elem.appendChild(document.createElement(elementName));
    if (txt !== null) {
      e.textContent = txt;
    }
    return e;
  };

  if ($('table thead tr th:nth-of-type(1)')) {
    // 順,期,date(投稿時刻),bbs,bbs名,key,ハンターID,ハンター名,ターゲット,subject
    // order,term,date,bbs,bbsname,key,id,hunter,target,subject
    const tr = $('table thead tr:nth-of-type(1)');
    $('th:nth-of-type(1)', tr).textContent = '順';
    $('th:nth-of-type(1)', tr).removeAttribute('style');
    $('th:nth-of-type(2)', tr).textContent = '期';
    $('th:nth-of-type(2)', tr).removeAttribute('style');
    appendEl(tr, 'date(投稿時刻)');
    appendEl(tr, 'bbs');
    appendEl(tr, 'bbs名');
    appendEl(tr, 'key');
    appendEl(tr, 'ハンターID');
    appendEl(tr, 'ハンター名');
    appendEl(tr, 'ターゲット');
    appendEl(tr, 'subject');

    $('table').insertAdjacentHTML('beforebegin', `<p>
  <ul>外部リンク:
    <li><a href="https://nanjya.net/donguri/" target="_blank">5chどんぐりシステムの備忘録</a> どんぐりシステムに関する詳細。</li>
    <li><a href="http://dongurirank.starfree.jp/" target="_blank">どんぐりランキング置き場</a> 大砲ログの過去ログ検索やログ統計など。</li>
  </ul>
</p>
<p>
  <ul><b>UserScriptの説明:</b>
    <li>以下の入力欄に<code>bbs=newsplus</code> のように入力すると、マッチするログのみ表示します。
    <li>入力欄の内容をURLのハッシュ(<code>#</code>)以降に同じように指定することでも機能します。
    <li>カンマ(<code>,</code>) で区切ることで複数条件の指定(論理積)ができます。
    <li>大砲ログの各セルをダブルクリックすることでも、その内容の条件を追加できます。
    <li>列ヘッダーをダブルクリックでソートします。
  </ul>
</p>`);
    const table = $('table');
    const headers = $$('th', table);
    const tbody = $('tbody', table);
    const rows = Array.from($$('tr', tbody));

    // 各列ヘッダーにダブルクリックイベントを設定
    headers.forEach((header, index) => {
      let sortOrder = 1; // 1: 自然順, -1: 逆順

      header.addEventListener('dblclick', () => {
        // クリックされた列のインデックスに基づいてソート
        rows.sort((rowA, rowB) => {
          const cellA = rowA.cells[index].textContent.trim();
          const cellB = rowB.cells[index].textContent.trim();

          // テキストで自然順ソート
          return cellA.localeCompare(cellB, 'ja', {
            numeric: true
          }) * sortOrder;
        });

        // ソート順を反転
        sortOrder *= -1;

        // ソート済みの行をtbodyに再配置
        rows.forEach(row => tbody.appendChild(row));
      });
    });
  }

  // Regular expression to detect and replace unwanted characters
  const rloRegex = /[\x00-\x1F\x7F\u200E\u200F\u202A\u202B\u202C\u202D\u202E]/g;
  const replaceTextRecursively = (element) => {
    element.childNodes.forEach(node => {
      if (node.nodeType === Node.TEXT_NODE) {
        node.textContent = node.textContent.replace(rloRegex, match => `[U+${match.codePointAt(0).toString(16).toUpperCase()}]`);
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        replaceTextRecursively(node);
      }
    });
  };



  $$('tr', $('table tbody')).forEach((tr,i) => {
    replaceTextRecursively(tr);
    const log = $('td:nth-of-type(2)', tr).textContent.trim();
    const verticalPos = log.lastIndexOf('|');
    const [bbs, key, date] = log.slice(verticalPos + 2).split(' ', 3);
    tr.dataset.order = i + 1;
    tr.dataset.term = $('td:nth-of-type(1)', tr).textContent.trim().slice(1, -1);
    tr.dataset.date = date;
    tr.dataset.bbs = bbs;
    tr.dataset.key = key;
    tr.dataset.log = log;
    const match = log.slice(0, verticalPos - 1).match(/^(.*)?さん\[([a-f0-9]{8})\]は(.*(?:\[[a-f0-9]{4}\*\*\*\*\])?)?さんを撃った$/);
    tr.dataset.id = match[2];
    tr.dataset.hunter = match[1];
    tr.dataset.target = match[3];

    $('td:nth-of-type(2)', tr).textContent = $('td:nth-of-type(1)', tr).textContent;
    $('td:nth-of-type(1)', tr).textContent = tr.dataset.order;
    appendEl(tr, addWeekdayToDatetime(date));
    appendEl(tr, bbs);
    appendEl(tr);
    appendEl(tr, key);
    appendEl(tr, tr.dataset.id);
    appendEl(tr, tr.dataset.hunter);
    appendEl(tr, tr.dataset.target);
    appendEl(tr);
  });

  GM_addStyle('body { margin: 0; padding: 12px; display: block; }');
  GM_addStyle('thead, tbody { white-space: nowrap; }');
  GM_addStyle('th { user-select: none; }');

  // Sanitize user input to avoid XSS and other injections
  const sanitize = (value) => value.replace(/[^a-zA-Z0-9_:/.\-]/g, '');

  // Update elements visibility based on filtering criteria
  const filterRows = (input) => {
    const value = input.value.trim();
    if (!value) {
      $$('tr', $('table tbody')).forEach(row => {
        row.style.display = '';
        return;
      });
      return;
    }

    const criteria = value.split(',').map(item => item.split('=')).reduce((acc, [key, val]) => {
      if (key && val) {
        acc[key.trim()] = key.match(/^(?:log|bbsname|hunter|target|subject)$/) ? val.trim() : sanitize(val.trim());
      }
      return acc;
    }, {});

    $$('tr', $('table tbody')).forEach(row => {
      const isVisible = Object.entries(criteria).every(([key, val]) => {
        if (key === 'ita') {
          key = 'bbs';
        }
        if (key === 'dat') {
          key = 'key';
        }
        if (row.hasAttribute(`data-${key}`)) {
          if (key.match(/^(?:term|bbs)$/)) {
            return row.getAttribute(`data-${key}`) === val;
          } else if (key.match(/^(?:log|bbsname|subject|date)$/)) {
            return row.getAttribute(`data-${key}`).includes(val);
          } else {
            return row.getAttribute(`data-${key}`).indexOf(val) === 0;
          }
        } else {
          return false;
        }
      });
      row.style.display = isVisible ? '' : 'none';
    });
  };

  // Initialize the filter input and its functionalities
  const createFilterInput = () => {
    const input = document.createElement('input');
    input.type = 'text';
    input.id = 'myfilter';
    input.placeholder = 'Filter (e.g., bbs=av, key=1711038453, date=06/01(土) 01:55, id=ac351e30, log=abesoriさん[97a65812])';
    input.style = 'width: 100%; padding: 5px; margin-bottom: 10px;';

    const table = $('table');
    if (table) {
      table.parentNode.insertBefore(input, table);
      input.addEventListener('input', () => {
        location.hash = '#' + input.value;
        return;
      });

      if (location.hash) {
        input.value = decodeURIComponent(location.hash.substring(1));
        filterRows(input);
      }

      window.addEventListener('hashchange', () => {
        input.value = decodeURIComponent(location.hash.substring(1));
        filterRows(input);
      });
    }
  };

  // Async function to wait until the subject list is loaded
  const waitForSubject = async (bbs, key) => {
    let retryCount = 0;
    while (retryCount < 30 && !(bbs in subjectList && `${key}.dat` in subjectList[bbs])) {
      await new Promise(resolve => setTimeout(resolve, 100));
      retryCount++;
    }
  };

  // GM_xmlhttpRequest wrapper to handle HTTP Get requests
  const getDat = (url, func, mime = 'text/plain; charset=shift_jis', tr = null) => {
    if (typeof tr === 'object' && url in completedURL) {
      const date = tr.dataset.date;
      const bbs = tr.dataset.bbs;
      const key = tr.dataset.key;
      (async () => {
        await waitForSubject(bbs, key);
        const origin = bbsList[bbs] || "https://origin";
        const bbsName = bbsList[`${bbs}_txt`] || "???";
        const subject = subjectList[bbs][`${key}.dat`] || "???";
        tr.dataset.origin = origin;
        tr.dataset.bbsname = bbsName;
        $('td:nth-of-type(5)', tr).textContent = bbsName;
        tr.dataset.subject = subject;
        const anchor = document.createElement('a');
        anchor.href = `${origin}/test/read.cgi/${bbs}/${key}/#date=${date}`;
        anchor.target = '_blank';
        anchor.textContent = subject;
        $('td:nth-of-type(10)', tr).insertAdjacentElement('beforeend', anchor);
      })();
      return;
    }
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      timeout: 86400 * 1000,
      overrideMimeType: mime,
      onload: function(response) {
        if (tr !== null) {
          const date = tr.dataset.date;
          const bbs = tr.dataset.bbs;
          const key = tr.dataset.key;
          func(response, tr, bbs, key, date);
        } else {
          func(response);
        }
      },
      onerror: function(error) {
        console.error('An error occurred during the request:', error);
      }
    });
  };

  const parser = new DOMParser();

  // Process subject line to update subject list and modify the row content
  const subjectFunc = (response, tr) => {
    const date = tr.dataset.date;
    const bbs = tr.dataset.bbs;
    const key = tr.dataset.key;
    completedURL[response.finalUrl] = true;
    if (response.status === 200) {
      const lastmodify = response.responseText;
      lastmodify.split(/[\r\n]+/).forEach(line => {
        let [key, subject] = line.split(/\s*<>\s*/, 2);
        if (/&#?[a-zA-Z0-9]+;?/.test(subject)) {
          subject = parser.parseFromString(subject, 'text/html').documentElement.innerText;
        }
        subjectList[bbs][key] = subject;
      });


      const origin = bbsList[bbs] || "https://origin";
      const bbsName = bbsList[`${bbs}_txt`] || "???";
      const subject = subjectList[bbs][`${key}.dat`] || "???";
      tr.dataset.origin = origin;
      tr.dataset.bbsname = bbsName;
      $('td:nth-of-type(5)', tr).textContent = bbsName;
      tr.dataset.subject = subject;
      const anchor = document.createElement('a');
      anchor.href = `${origin}/test/read.cgi/${bbs}/${key}/#date=${date}`;
      anchor.target = '_blank';
      anchor.textContent = subject;
      $('td:nth-of-type(10)', tr).insertAdjacentElement('beforeend', anchor);
    } else {
      console.error('Failed to load data. Status code:', response.status);
    }
  };

  // Function to handle each table row for subject processing
  const nextFunc = async () => {
    $$('tr', $('table tbody')).forEach(tr => {
      const bbs = tr.dataset.bbs;

      if (!Object.hasOwn(subjectList, bbs)) {
        subjectList[bbs] = {};
      }
      getDat(`${bbsList[bbs]}/${bbs}/lastmodify.txt`, subjectFunc, 'text/plain; charset=shift_jis', tr);
    });
    createFilterInput();
    document.querySelector('table').addEventListener('dblclick', function(event) {
      event.preventDefault();
      const target = event.target;
      if (target.tagName === 'TD') {
        const index = Array.prototype.indexOf.call(target.parentNode.children, target);
        const txt = `${column[index]}=${target.textContent}`;
        if (!$('#myfilter')) {
          return;
        }
        location.hash += location.hash.indexOf('=') > -1 ? `,${txt}` : txt;
      }
    });
  };

  // Function to process the bbsmenu response
  const bbsmenuFunc = (response) => {
    if (response.status === 200) {
      const html = document.createElement('html');
      html.innerHTML = response.responseText;
      $$('a[href*=".5ch.net/"],a[href*=".bbspink.com/"]', html).forEach(bbsLink => {
        const match = bbsLink.href.match(/\.(?:5ch\.net|bbspink\.com)\/([a-zA-Z0-9_-]+)\/$/);
        if (match) {
          bbsList[match[1]] = new URL(bbsLink.href).origin;
          bbsList[`${match[1]}_txt`] = bbsLink.textContent.trim();
        }
      });
      if (Object.keys(bbsList).length === 0) {
        console.error('No boards found.');
        return;
      }
      nextFunc();
    } else {
      console.error('Failed to fetch bbsmenu. Status code:', response.status);
    }
  };

  // Initial data fetch from bbsmenu
  getDat('https://menu.5ch.net/bbsmenu.html', bbsmenuFunc, 'text/html; charset=shift_jis');
})();