「MediaWiki:Forum.js」の版間の差分
編集の要約なし |
編集の要約なし |
||
| 7行目: | 7行目: | ||
// 設定がない場合のデフォルト値 | // 設定がない場合のデフォルト値 | ||
mw.forum = mw.forum || {}; | mw.forum = mw.forum || {}; | ||
mw.forum.toppage = mw.forum.toppage || 'KenryoBBS1'; | mw.forum.toppage = mw.forum.toppage || 'KenryoBBS1'; | ||
mw.forum.threads = mw.forum.threads || 20; | mw.forum.threads = mw.forum.threads || 20; | ||
mw.forum.sticky = mw.forum.sticky || []; | |||
const fedit = mw.util.getParamValue('fedit'); | const fedit = mw.util.getParamValue('fedit'); | ||
// ページ判定(掲示板ページでなければ終了) | // ページ判定(掲示板ページでなければ終了) | ||
if(!new RegExp(`^${mw.forum.toppage}(|/.*)$`).test(mw.config.get('wgPageName')) || mw.config.get('wgAction') != 'view'){ | if(!new RegExp(`^${mw.forum.toppage}(|/.*)$`).test(mw.config.get('wgPageName')) || mw.config.get('wgAction') != 'view'){ | ||
return; | return; | ||
| 41行目: | 40行目: | ||
const isAdmin = mw.config.get('wgUserGroups').includes('sysop'); | const isAdmin = mw.config.get('wgUserGroups').includes('sysop'); | ||
// | // ユーザー名(IPなど)をハッシュ化して動物インデックスに変換 | ||
const stringToHash = (str) => { | const stringToHash = (str) => { | ||
let hash = 0; | let hash = 0; | ||
| 52行目: | 51行目: | ||
return Math.abs(hash); | return Math.abs(hash); | ||
}; | }; | ||
// ========== トリップ機能 ========== | |||
// SHA-256を使ってトリップコードを生成する | |||
// 入力: 秘密のキー文字列 | |||
// 出力: "◆" + Base64の先頭10文字 | |||
const computeTrip = async (key) => { | |||
const encoder = new TextEncoder(); | |||
const data = encoder.encode(key); | |||
const hashBuffer = await crypto.subtle.digest('SHA-256', data); | |||
const bytes = new Uint8Array(hashBuffer); | |||
// バイト列をBase64文字列に変換 | |||
let binary = ''; | |||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); | |||
const base64 = btoa(binary); | |||
return '◆' + base64.slice(0, 10); | |||
}; | |||
// localStorageから匿名ユーザーの名前設定を読み込む | |||
const loadAnonSettings = () => { | |||
try { | |||
const saved = localStorage.getItem('forum_anon_settings'); | |||
return saved ? JSON.parse(saved) : null; | |||
} catch(e) { return null; } | |||
}; | |||
// 匿名ユーザーの名前設定をlocalStorageに保存する | |||
const saveAnonSettings = (name, tripKey) => { | |||
try { | |||
localStorage.setItem('forum_anon_settings', JSON.stringify({ name, tripKey })); | |||
} catch(e) {} | |||
}; | |||
// ====================================== | |||
(async()=>{ | (async()=>{ | ||
if (isAnon) { | if (isAnon) { | ||
// デフォルトの匿名名(IPハッシュベース) | |||
const identifier = mw.user.getName() || mw.user.sessionId(); | const identifier = mw.user.getName() || mw.user.sessionId(); | ||
const index = stringToHash(identifier) % animals.length; | const index = stringToHash(identifier) % animals.length; | ||
const defaultName = '匿名' + animals[index]; | |||
// 保存済み設定があればそれを使う | |||
const saved = loadAnonSettings(); | |||
mw.forum.anonDefaultName = defaultName; | |||
mw.forum.anonName = (saved && saved.name) ? saved.name : defaultName; | |||
mw.forum.anonTripKey = (saved && saved.tripKey) ? saved.tripKey : ''; | |||
// トリップがあれば計算してusernameに反映 | |||
if (mw.forum.anonTripKey) { | |||
const trip = await computeTrip(mw.forum.anonTripKey); | |||
mw.forum.username = mw.forum.anonName + trip; | |||
} else { | |||
mw.forum.username = mw.forum.anonName; | |||
} | |||
} else { | } else { | ||
mw.forum.username = mw.config.get('wgUserName'); | mw.forum.username = mw.config.get('wgUserName'); | ||
} | } | ||
if(!mw.forum.username) { | if(!mw.forum.username) { | ||
// フォールバック | // フォールバック | ||
| 77行目: | 124行目: | ||
// メイン処理関数 | // メイン処理関数 | ||
function startForum() { | function startForum() { | ||
// ========== 名前・トリップ設定UIのHTML ========== | |||
// 匿名ユーザーのみ投稿フォームの上部に表示される | |||
const anonSettingsHtml = isAnon ? `<details id="f-name-settings" style="margin-bottom:.8em;border:1px solid #ccc;padding:.5em;border-radius:3px;"> | |||
<summary style="cursor:pointer;font-weight:bold;user-select:none;">▼ 名前・トリップの設定</summary> | |||
<div style="margin-top:.5em;"> | |||
<p style="margin:.2em 0 .5em;font-size:.9em;color:#555;"> | |||
トリップキーを設定すると、投稿名の後ろに <b>◆記号+固有コード</b> が付き、なりすましを防げます。<br> | |||
キーは他人に見えませんが、同じキーからは常に同じコードが生成されます。 | |||
</p> | |||
<label style="display:block;margin-bottom:.4em;"> | |||
名前: | |||
<input type="text" id="f-anon-name" class="mw-ui-input" placeholder="例: 名無しさん" style="width:200px;display:inline-block;margin-left:.5em;"> | |||
</label> | |||
<label style="display:block;margin-bottom:.4em;"> | |||
トリップキー: | |||
<input type="password" id="f-anon-trip" class="mw-ui-input" placeholder="秘密のキー(省略可)" style="width:200px;display:inline-block;margin-left:.5em;" autocomplete="new-password"> | |||
</label> | |||
<span id="f-trip-preview" style="font-size:.9em;color:#555;display:block;margin-bottom:.4em;"></span> | |||
<input type="button" id="f-save-name" value="設定を保存" class="mw-ui-button"> | |||
</div> | |||
</details>` : ''; | |||
// ================================================= | |||
const msg = Object.assign({ | const msg = Object.assign({ | ||
loading: '<p>読み込み中...</p>', | loading: '<p>読み込み中...</p>', | ||
postform: `<div id="f-form"><h2>投稿${fedit ? `の編集 (<a href="#post-${fedit}")>#${fedit}</a>)` : ''}</h2><div class="mw-ui-checkbox" style="margin-bottom:.3em;${fedit ? 'display:none;' : ''}"><input type="checkbox" class="mw-ui-checkbox" id="f-reply-cb"><label for="f-reply-cb" style="user-select:none;">返信する</label></div><input type="number" id="f-reply" class="mw-ui-input" style="widht:50%;margin-bottom:.5em;"><textarea accesskey="," id="wpTextbox1" cols="80" rows="25" class="mw-editfont-monospace"></textarea><input type="button" id="f-post" value="投稿" class="mw-ui-button mw-ui-progressive" style="margin-top:.5em;"><input type="button" id="f-preview" value="プレビュー" class="mw-ui-button" style="margin: .5em 0 0 .5em;"><fieldset hidden><legend>プレビュー</legend><div id="f-preview-content"></div></fieldset><style>.mw-ui-checkbox:has(#f-reply-cb:checked)+input{display:block;}#f-reply{display:none;}</style></div>`, | postform: `<div id="f-form"><h2>投稿${fedit ? `の編集 (<a href="#post-${fedit}")>#${fedit}</a>)` : ''}</h2>${anonSettingsHtml}<div class="mw-ui-checkbox" style="margin-bottom:.3em;${fedit ? 'display:none;' : ''}"><input type="checkbox" class="mw-ui-checkbox" id="f-reply-cb"><label for="f-reply-cb" style="user-select:none;">返信する</label></div><input type="number" id="f-reply" class="mw-ui-input" style="widht:50%;margin-bottom:.5em;"><textarea accesskey="," id="wpTextbox1" cols="80" rows="25" class="mw-editfont-monospace"></textarea><input type="button" id="f-post" value="投稿" class="mw-ui-button mw-ui-progressive" style="margin-top:.5em;"><input type="button" id="f-preview" value="プレビュー" class="mw-ui-button" style="margin: .5em 0 0 .5em;"><fieldset hidden><legend>プレビュー</legend><div id="f-preview-content"></div></fieldset><style>.mw-ui-checkbox:has(#f-reply-cb:checked)+input{display:block;}#f-reply{display:none;}</style></div>`, | ||
postsummary: 'post', | postsummary: 'post', | ||
replysummary: 'reply to', | replysummary: 'reply to', | ||
| 151行目: | 222行目: | ||
mw.notify(msg.deleteerror); | mw.notify(msg.deleteerror); | ||
}); | }); | ||
}, | |||
// ========== 名前設定UIの初期化 ========== | |||
initAnonSettings: () => { | |||
if (!isAnon) return; | |||
const nameInput = document.querySelector('#f-anon-name'); | |||
const tripInput = document.querySelector('#f-anon-trip'); | |||
const preview = document.querySelector('#f-trip-preview'); | |||
const saveBtn = document.querySelector('#f-save-name'); | |||
if (!nameInput || !tripInput) return; | |||
// 保存済み値をフォームに反映 | |||
nameInput.value = mw.forum.anonName; | |||
tripInput.value = mw.forum.anonTripKey; | |||
// プレビュー更新関数 | |||
const updatePreview = async () => { | |||
const name = nameInput.value.trim() || mw.forum.anonDefaultName; | |||
const key = tripInput.value; | |||
if (key) { | |||
const trip = await computeTrip(key); | |||
preview.textContent = `投稿名プレビュー: ${name}${trip}`; | |||
} else { | |||
preview.textContent = `投稿名プレビュー: ${name}`; | |||
} | |||
}; | |||
nameInput.addEventListener('input', updatePreview); | |||
tripInput.addEventListener('input', updatePreview); | |||
updatePreview(); // 初期表示 | |||
// 保存ボタン | |||
saveBtn.onclick = async () => { | |||
const newName = nameInput.value.trim() || mw.forum.anonDefaultName; | |||
const newTripKey = tripInput.value; | |||
saveAnonSettings(newName, newTripKey); | |||
mw.forum.anonName = newName; | |||
mw.forum.anonTripKey = newTripKey; | |||
if (newTripKey) { | |||
const trip = await computeTrip(newTripKey); | |||
mw.forum.username = newName + trip; | |||
} else { | |||
mw.forum.username = newName; | |||
} | |||
mw.notify('名前設定を保存しました。次の投稿から反映されます。'); | |||
}; | |||
} | } | ||
// ========================================= | |||
}; | }; | ||
const mc = document.querySelector('#mw-content-text>.mw-parser-output'); | const mc = document.querySelector('#mw-content-text>.mw-parser-output'); | ||
if(!mc) return; | if(!mc) return; | ||
if(mw.config.get('wgPageName') == mw.forum.toppage){ | if(mw.config.get('wgPageName') == mw.forum.toppage){ | ||
| 168行目: | 284行目: | ||
content += `{{post|System|0|{{subst:#timel:Y/m/d H:i}}|4=${mw.forum.zeroTemplate}}}\n`; | content += `{{post|System|0|{{subst:#timel:Y/m/d H:i}}|4=${mw.forum.zeroTemplate}}}\n`; | ||
} | } | ||
content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}}}`; | // スレッド作成者がログイン済みかどうかで anon フラグを付与 | ||
const anonParam = isAnon ? '|anon=1' : ''; | |||
content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}${anonParam}}}`; | |||
new mw.Api().postWithToken('csrf', { | new mw.Api().postWithToken('csrf', { | ||
action: 'edit', | action: 'edit', | ||
| 214行目: | 332行目: | ||
const indicators = document.querySelector('.mw-indicators'); | const indicators = document.querySelector('.mw-indicators'); | ||
if(indicators) indicators.innerHTML = msg.gotoform; | if(indicators) indicators.innerHTML = msg.gotoform; | ||
// 名前設定UIを初期化(匿名ユーザーのみ) | |||
func.initAnonSettings(); | |||
document.querySelector('#f-post').onclick = (async() => { | document.querySelector('#f-post').onclick = (async() => { | ||
| 228行目: | 349行目: | ||
summary = `${msg.postsummary} [[${mw.config.get('wgPageName')}#post-${lp}|#${lp}]]`; | summary = `${msg.postsummary} [[${mw.config.get('wgPageName')}#post-${lp}|#${lp}]]`; | ||
} | } | ||
// 匿名ユーザーには |anon=1 を付与してテンプレートでリンクを非表示にする | |||
const anonParam = isAnon ? '|anon=1' : ''; | |||
new mw.Api().postWithToken('csrf', { | new mw.Api().postWithToken('csrf', { | ||
action: 'edit', | action: 'edit', | ||
title: mw.config.get('wgPageName'), | title: mw.config.get('wgPageName'), | ||
[fedit ? 'text' : 'appendtext']: fedit ? source.replace(new RegExp(`\\{\\{post\\|(.*?)\\|${fedit}\\|(.*?)\\|4=((.|\n)*?)}}`), `{{post|$1|${fedit}|$2|4=${document.querySelector('#wpTextbox1').value}}}`) :`\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re='+document.querySelector('#f-reply').value : ''}}}`, | [fedit ? 'text' : 'appendtext']: fedit | ||
? source.replace(new RegExp(`\\{\\{post\\|(.*?)\\|${fedit}\\|(.*?)\\|4=((.|\n)*?)}}`), `{{post|$1|${fedit}|$2|4=${document.querySelector('#wpTextbox1').value}}}`) | |||
: `\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re='+document.querySelector('#f-reply').value : ''}${anonParam}}}`, | |||
summary: summary, | summary: summary, | ||
format: 'json' | format: 'json' | ||
| 281行目: | 408行目: | ||
}; | }; | ||
if (window.mw && window.mw.loader) { | if (window.mw && window.mw.loader) { | ||
checkConfig(); | checkConfig(); | ||
} else { | } else { | ||
window.addEventListener('load', checkConfig); | window.addEventListener('load', checkConfig); | ||
} | } | ||
})(); | })(); | ||
// </nowiki> | // </nowiki> | ||