彁駲槞2025


309426

今日239 昨日0

彁駲槞2025公式ブログ

投稿数ランキング

名前
題名
内容
画像
文字    赤    青
   紫    黄

[615] 題名: 名前:コーラン 2025年10月09日(木) 17:43:42

ルフィ「ナミ、ナミ、新聞」

いいね(2)


[614] 題名:エピソードオブゾロ 名前:ぽめ@麦わらの一味 2025年10月09日(木) 17:29:20

ゾロはいいよね。

いいね(0)


[613] 題名: 名前:たけしま 2025年10月09日(木) 17:09:26

いいね(0)


[612] 題名: 名前:たけしま 2025年10月09日(木) 17:03:41

来ない、闇の陰謀?

いいね(0)


[611] 題名: 名前:たけしま 2025年10月09日(木) 16:52:52

いいね(0)


[610] 題名:@つぼみ 名前:愛子 2025年10月09日(木) 16:47:27

1日活動禁止な

いいね(1)


[609] 題名:ありがとう 名前:しっふぃー 2025年10月09日(木) 15:39:29

テスト

いいね(0)


[608] 題名: 名前:毒カレー 2025年10月09日(木) 15:37:51

ありがとう

いいね(1)


[607] 題名:通知が来ます 名前:つぼみ 2025年10月09日(木) 15:35:28

/************************************************************
* 掲示板監視システム(色抽出+カラー記録対応)
* - 本文内の <font color="#XXXXXX"> を抽出し log に記録
************************************************************/

const SHEETS = {
CONFIG: 'config',
EMAILS: 'emails',
STATE: 'state',
LOG: 'log'
};

// === メイン ===
function monitorLatestOnly_() {
const ss = SpreadsheetApp.getActive();
const cfgRows = readConfigRows_(ss);
const recipients = readEmails_(ss);
if (recipients.length === 0) return;

const stateMap = readStateMap_(ss);

cfgRows.forEach(row => {
if (!row.enabled) return;
try {
const { html } = fetchHtml_(row.url, row.ua, row.timeoutSec);
const posts = extractAllPosts_(html);
if (!posts.length) {
appendLog_(ss, row.url, 'ERROR: 投稿が検出できませんでした');
return;
}

const prev = stateMap.get(row.url) || { hash: '', content: '' };
const prevNo = parseInt(prev.hash || '0', 10) || 0;
const newPosts = posts.filter(p => p.no > prevNo);
if (newPosts.length === 0) return;

sendPostMail_(recipients, row, newPosts);
appendLog_(ss, row.url, newPosts);

const last = Math.max(...newPosts.map(p => p.no));
upsertState_(ss, row.url, String(last), newPosts[newPosts.length - 1].body);

} catch (e) {
const msg = `ERROR: ${e && e.message ? e.message : e}`;
appendLog_(ss, row.url, msg);
Logger.log(msg);
}
});
}

/************************************************************
* HTML解析(投稿番号・題名・名前・本文抽出)
************************************************************/
function extractAllPosts_(html) {
const h = (html || '').replace(/\r\n?/g, '\n');
const re = /\[(\d{1,6})\][\s\S]{0,200}?題名:\s*([^<\n]*)\s*名前:\s*([^<\n]*)[\s\S]*?<blockquote[^>]*>([\s\S]*?)<\/blockquote>[\s\S]{0,300}?id=["']?upload\1\b/gi;
const results = [];
let m;
while ((m = re.exec(h)) !== null) {
const no = parseInt(m[1], 10);
const title = (m[2] || '').trim() || '(無題)';
const name = (m[3] || '').trim() || '(名無し)';
let inner = m[4] || '';

// カラーコード抽出(HTML内 <font color="...">)
let colorMatch = inner.match(/<font[^>]+color=["']?(#[0-9A-Fa-f]{6})["']?/i);
const color = colorMatch ? colorMatch[1].toUpperCase() : '(none)';

// 本文抽出
const pm = inner.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
if (pm) inner = pm[1];
const body = inner
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/[ \t]+/g, ' ')
.replace(/\n{3,}/g, '\n\n')
.trim();

if (body) results.push({ no, title, name, body, color });
}

results.sort((a, b) => a.no - b.no);
return results;
}

/************************************************************
* メール送信(HTML色分け+件名タグ・To送信方式)
************************************************************/
function sendPostMail_(recipients, row, newPosts) {
const subjectTag = '📝 新規投稿通知';

let htmlBody = `
<div style="
font-family:'Helvetica','Arial','Meiryo',sans-serif;
color:#333;
background:#fafafa;
border:1px solid #ddd;
border-radius:8px;
padding:20px;
position:relative;
">
<h2 style="margin:0 0 15px 0; color:#0066cc; font-size:18px;">
新しい投稿が${newPosts.length}件見つかりました
</h2>

<p style="margin:0 0 20px 0; font-size:14px; line-height:1.5;">
以下は最新の投稿です。
</p>
`;

// 各投稿を整形して追加
newPosts.forEach(p => {
const preview = p.body.length > 300 ? p.body.slice(0, 300) + ' …' : p.body;

// 背景色を種類別に変更(淡く・控えめに)
let bgColor = '#ffffff';
if (/襲撃報告/.test(p.title)) bgColor = '#fdf5f5';
else if (/お知らせ/.test(p.title)) bgColor = '#fffdf2';
else if (/アクセス報告/.test(p.title)) bgColor = '#f7fff7';

htmlBody += `
<div style="
background:${bgColor};
border:1px solid #ddd;
border-radius:6px;
padding:12px 16px;
margin-bottom:15px;
">
<div style="font-size:13px; color:#777;">No.${p.no}</div>
<div style="font-size:15px; font-weight:bold; color:#333; margin-top:3px;">
${p.title}
</div>
<div style="font-size:13px; color:#555; margin-top:4px;">
投稿者:${p.name}
</div>
<div style="
margin-top:8px;
font-size:14px;
color:#333;
line-height:1.6;
white-space:pre-wrap;
">${preview}</div>
</div>
`;
});

// フッター部分
htmlBody += `
<p style="font-size:12px; color:#888; text-align:center; margin-top:25px;">
────────────────────────

自動監視システムより通知

<a href="${row.url}" style="color:#0066cc;">${row.url}</a>
</p>
</div>
`;

// ✉️ 通常送信(To)
MailApp.sendEmail({
to: recipients.join(','), // 全アドレスをToに
subject: `${row.subjectPrefix || '【新投稿通知】'} ${subjectTag}(${newPosts.length}件)`,
htmlBody: htmlBody,
name: "Canada2025 自動監視システム"
});
}
/************************************************************
* データ入出力(★ログの堅牢化+詳細ログ出力)
************************************************************/
function appendLog_(ss, url, postsOrError) {
const sh = ss.getSheetByName(SHEETS.LOG);
const now = new Date();

// === 1️⃣ 空白行を自動で詰める ===
const lastRow = sh.getLastRow();
if (lastRow > 1) {
const data = sh.getRange(2, 1, lastRow - 1, 7).getValues();
const filled = data.filter(r => r.join('').trim() !== ''); // 空白行除去
sh.getRange(2, 1, sh.getMaxRows() - 1, 7).clearContent(); // 既存データ削除
if (filled.length > 0) {
sh.getRange(2, 1, filled.length, 7).setValues(filled); // 上詰め
}
}

// === 2️⃣ 投稿データの書き込み ===
if (Array.isArray(postsOrError)) {
if (postsOrError.length === 0) return;

const rows = postsOrError.map(p => [
now,
url,
p.no,
p.title,
p.name,
p.body.length > 300 ? p.body.slice(0, 300) + ' …' : p.body,
p.color || '(none)'
]);

const newLast = sh.getLastRow();
sh.getRange(newLast + 1, 1, rows.length, rows[0].length).setValues(rows);
return;
}

// === 3️⃣ エラー時 ===
const errMsg = typeof postsOrError === 'string' ? postsOrError : JSON.stringify(postsOrError);
const newLast = sh.getLastRow();
sh.getRange(newLast + 1, 1, 1, 7).setValues([
[now, url, 'ERROR', '', '', errMsg.slice(0, 500), '(none)']
]);
}

/************************************************************
* その他共通関数(config, email, state)
************************************************************/
function readConfigRows_(ss) {
const sh = ss.getSheetByName(SHEETS.CONFIG);
const values = sh.getDataRange().getValues().slice(1);
return values.map(r => ({
url: r[0],
selector: r[1] || '',
enabled: String(r[2]).toUpperCase() === 'TRUE',
ignore: r[3],
contextLines: r[4] || 3,
diffMax: r[5] || 4000,
ua: r[6] || 'Mozilla/5.0 (compatible; GAS Monitor)',
timeoutSec: r[7] || 30,
subjectPrefix: r[8] || '【監視通知】'
})).filter(r => r.url);
}

function readEmails_(ss) {
const sh = ss.getSheetByName(SHEETS.EMAILS);
return sh.getDataRange().getValues().slice(1).map(r => r[0]).filter(x => x);
}

function readStateMap_(ss) {
const sh = ss.getSheetByName(SHEETS.STATE);
const map = new Map();
const values = sh.getDataRange().getValues().slice(1);
values.forEach(r => {
if (r[0]) map.set(r[0], { hash: r[1], content: r[2] });
});
return map;
}

function upsertState_(ss, url, hash, content) {
const sh = ss.getSheetByName(SHEETS.STATE);
const lastRow = sh.getLastRow();
const values = sh.getRange(2, 1, Math.max(0, lastRow - 1), 3).getValues();
for (let i = 0; i < values.length; i++) {
if (values[i][0] === url) {
sh.getRange(i + 2, 2, 1, 2).setValues([[hash, content]]);
return;
}
}
sh.appendRow([url, hash, content]);
}

function fetchHtml_(url, ua, timeoutSec) {
const html = UrlFetchApp.fetch(url, {
method: 'get',
muteHttpExceptions: true,
headers: { 'User-Agent': ua },
timeout: timeoutSec * 1000
}).getContentText('UTF-8');
return { html };
}

/************************************************************
* テスト送信用
************************************************************/
function testSendMail() {
const ss = SpreadsheetApp.getActive();
const recipients = readEmails_(ss);
if (recipients.length === 0) {
Logger.log("⚠ 宛先が設定されていません(emailsシートを確認)");
return;
}

const dummyPosts = [
{ no: 201, title: '襲撃報告:北部警戒', name: 'りん', body: '警戒区域に不審な影あり。', color: '#FF0000' },
{ no: 202, title: 'お知らせ:新ルール発表', name: '管理人', body: '本日よりルールが改訂されます。', color: '#FFFF00' },
{ no: 203, title: 'アクセス報告:通常通り', name: 'システム', body: '問題なし。安定稼働中。', color: '#00FF00' }
];

const row = {
url: 'https://bbs1.sekkaku.net/bbs/canada2025/',
subjectPrefix: '【テスト通知】'
};

sendPostMail_(recipients, row, dummyPosts);
Logger.log('✅ テストメールを送信しました。カラー抽出付き。');
}

/************************************************************
* 🔧 appendLog_ の動作テスト関数
* - log シートへの書き込み確認用
************************************************************/
function testAppendLog() {
const ss = SpreadsheetApp.getActiveSpreadsheet();

const dummyPosts = [
{
no: 999,
title: 'テスト投稿:ログ確認',
name: 'テスター',
body: 'これはappendLog_関数のテストです。',
color: '#00AAFF'
},
{
no: 1000,
title: 'テスト投稿:2件目',
name: '確認者',
body: 'logシートに書き込まれればOKです。',
color: '#FFAA00'
}
];

appendLog_(ss, 'https://bbs1.sekkaku.net/bbs/canada2025/', dummyPosts);
Logger.log('🧪 testAppendLog 実行完了。logシートを確認してください。');
}

/************************************************************
* 🕒 1分ループ監視トリガー(復活版)
* - 「startOneMinuteLoop」で1分後に自動再実行
* - 「stopOneMinuteLoop」で停止
************************************************************/

/** ループ起動 */
function startOneMinuteLoop() {
try {
monitorLatestOnly_();
} catch (e) {
Logger.log("⚠ monitorLatestOnly_ 実行中エラー: " + e);
}

// 既存の自分トリガーを削除(多重登録防止)
const triggers = ScriptApp.getProjectTriggers();
for (const t of triggers) {
if (t.getHandlerFunction() === 'startOneMinuteLoop') {
ScriptApp.deleteTrigger(t);
}
}

// 1分後に再実行
ScriptApp.newTrigger('startOneMinuteLoop')
.timeBased()
.after(60 * 1000)
.create();

Logger.log("✅ 1分ループが起動しました。停止する場合は stopOneMinuteLoop() を実行してください。");
}

/** ループ停止 */
function stopOneMinuteLoop() {
const triggers = ScriptApp.getProjectTriggers();
let removed = 0;
for (const t of triggers) {
if (t.getHandlerFunction() === 'startOneMinuteLoop') {
ScriptApp.deleteTrigger(t);
removed++;
}
}
Logger.log(`🛑 停止しました。削除トリガー数: ${removed}`);
}

いいね(1)


[606] 題名: 名前:WiiU 2025年10月09日(木) 15:13:59

だから、玉効果を説明する必要があったんですね。

いいね(0)


[605] 題名:お知らせ 名前:モノ@マスター 2025年10月09日(木) 15:02:27

襲撃のやり方を公開しました。
襲撃処理に不安がある方はぜひ参考にしてください。

https://canada2025.livedoor.blog/archives/10137220.html

いいね(2)


[604] 題名:テスト 名前:じぽ@マスター 2025年10月09日(木) 14:19:55

テスト

いいね(0)


[603] 題名: 名前:ヒョン 2025年10月09日(木) 12:32:52

もどり

いいね(0)


[602] 題名:これは 名前:しっふぃー 2025年10月09日(木) 12:29:38

長いテストになりそうだ

いいね(0)


[601] 題名: 名前:ヒョン 2025年10月09日(木) 12:29:16

2分くらい離席

いいね(0)


[600] 題名:用具受け渡しについて 名前:ヒョン@マスター 2025年10月09日(木) 12:07:58

本日の用具受け渡しは学生会館E739にて現在から14:40まで行います。
本日までに必ず受け取りを済ませてください。

いいね(0)


[599] 題名:私も 名前:しっふぃー 2025年10月09日(木) 12:05:02

テストや

いいね(0)


[598] 題名:黒柳徹子でございます本日の 名前:WiiU 2025年10月09日(木) 10:18:45

ゲスト

いいね(0)


[597] 題名: 名前:哀真寿 2025年10月09日(木) 09:18:16

テスト

いいね(0)


[596] 題名:ルールについて 名前:じぽ@マスター 2025年10月09日(木) 08:54:15

後日公開としていたすべてのフォームについて、追記し公開いたしました。ご確認ください。

https://canada2025.livedoor.blog/archives/10037358.html

いいね(0)


[595] 題名:お知らせ 名前:じぽ@マスター 2025年10月09日(木) 08:46:15

公式LINEの閠機能、襲撃手順機能、リンク機能が利用できるようになりました。ご確認ください。

いいね(0)


[594] 題名:掲示板投稿通知について 名前:じぽ@マスター 2025年10月09日(木) 08:31:14

8:30現在までに掲示板通知申請フォームを送信された方のメールアドレスを登録いたしました。送信したが通知が来ないという方は、お手数おかけしますがメールアドレスを変えて再度申請していただくようお願いします。

まだ掲示板通知の申請がお済みでない方は、以下の記事をご確認ください。

https://canada2025.livedoor.blog/archives/10084240.html

いいね(0)


[593] 題名: 名前:つぼみ 2025年10月09日(木) 05:45:08

いいね(0)


[592] 題名: 名前:つぼみ 2025年10月09日(木) 03:41:44

すごいことが起きてる

いいね(1)


[591] 題名: 名前:つぼみ 2025年10月09日(木) 03:35:00

テスト

いいね(0)




SYSTEM BY せっかく掲示板