From 847b1222d0ecc70b220990d030dfbc45317ad944 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 5 Jan 2026 07:36:32 +0800 Subject: [PATCH 1/7] block: temporary account enhancement - Auto-acquire and display TA expiry date - Hide period options beyond TA expiry date - Add "De facto infinity" option that sets block expiry to TA expiry - Verify custom block period doesn't exceed TA expiry date, otherwise ask to confirm --- modules/twinkleblock.js | 197 ++++++++++++++++++++++++++++++++++------ 1 file changed, 170 insertions(+), 27 deletions(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index 1cc64f8b..6bc072ce 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -242,8 +242,21 @@ Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) { Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map(function(e) { return e.group; }).indexOf('bot') !== -1; + + Twinkle.block.isTempAccount = !!userinfo.groups && userinfo.groups.indexOf('temp') !== -1; + if (Twinkle.block.isTempAccount && userinfo.registration) { + var registrationDate = new Date(userinfo.registration); + Twinkle.block.tempAccountRegistration = registrationDate; + Twinkle.block.tempAccountExpiry = new Date(registrationDate.getTime() + (90 * 24 * 60 * 60 * 1000)); + } else { + Twinkle.block.tempAccountRegistration = null; + Twinkle.block.tempAccountExpiry = null; + } } else { Twinkle.block.userIsBot = false; + Twinkle.block.isTempAccount = false; + Twinkle.block.tempAccountRegistration = null; + Twinkle.block.tempAccountExpiry = null; } if (blockinfo) { @@ -290,7 +303,7 @@ Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) { } else { query.bkusers = Morebits.relevantUserName(true); // groupmemberships only relevant for registered users - query.usprop = 'groupmemberships'; + query.usprop = 'groupmemberships|registration|groups'; } api.get(query).then(function(data) { @@ -441,24 +454,7 @@ Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction name: 'expiry_preset', label: conv({ hans: '过期时间:', hant: '過期時間:' }), event: Twinkle.block.callback.change_expiry, - list: [ - { label: conv({ hans: '自定义', hant: '自訂' }), value: 'custom', selected: true }, - { label: conv({ hans: '无限期', hant: '無限期' }), value: 'infinity' }, - { label: conv({ hans: '3小时', hant: '3小時' }), value: '3 hours' }, - { label: conv({ hans: '12小时', hant: '12小時' }), value: '12 hours' }, - { label: '1天', value: '1 day' }, - { label: conv({ hans: '31小时', hant: '31小時' }), value: '31 hours' }, - { label: '2天', value: '2 days' }, - { label: '3天', value: '3 days' }, - { label: conv({ hans: '1周', hant: '1週' }), value: '1 week' }, - { label: conv({ hans: '2周', hant: '2週' }), value: '2 weeks' }, - { label: conv({ hans: '1个月', hant: '1個月' }), value: '1 month' }, - { label: conv({ hans: '3个月', hant: '3個月' }), value: '3 months' }, - { label: conv({ hans: '6个月', hant: '6個月' }), value: '6 months' }, - { label: '1年', value: '1 year' }, - { label: '2年', value: '2 years' }, - { label: '3年', value: '3 years' } - ] + list: Twinkle.block.callback.generateExpiryList() }); field_block_options.append({ type: 'input', @@ -468,6 +464,14 @@ Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry }); + if (Twinkle.block.isTempAccount && Twinkle.block.tempAccountExpiry) { + field_block_options.append({ + type: 'div', + name: 'tempaccountinfo', + label: conv({ hans: '临时账号到期时间:', hant: '臨時帳號到期時間:' }) + Twinkle.block.tempAccountExpiry.toLocaleString() + }); + } + if (partialBox) { // Partial block field_block_options.append({ type: 'select', @@ -1644,16 +1648,141 @@ Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset } }; +Twinkle.block.callback.generateExpiryList = function twinkleblockCallbackGenerateExpiryList() { + var allOptions = [ + { label: conv({ hans: '自定义', hant: '自訂' }), value: 'custom', selected: true }, + { label: conv({ hans: '无限期', hant: '無限期' }), value: 'infinity' }, + { label: conv({ hans: '3小时', hant: '3小時' }), value: '3 hours' }, + { label: conv({ hans: '12小时', hant: '12小時' }), value: '12 hours' }, + { label: '1天', value: '1 day' }, + { label: conv({ hans: '31小时', hant: '31小時' }), value: '31 hours' }, + { label: '2天', value: '2 days' }, + { label: '3天', value: '3 days' }, + { label: conv({ hans: '1周', hant: '1週' }), value: '1 week' }, + { label: conv({ hans: '2周', hant: '2週' }), value: '2 weeks' }, + { label: conv({ hans: '1个月', hant: '1個月' }), value: '1 month' }, + { label: conv({ hans: '3个月', hant: '3個月' }), value: '3 months' }, + { label: conv({ hans: '6个月', hant: '6個月' }), value: '6 months' }, + { label: '1年', value: '1 year' }, + { label: '2年', value: '2 years' }, + { label: '3年', value: '3 years' } + ]; + + // not a temp account: all options + if (!Twinkle.block.isTempAccount || !Twinkle.block.tempAccountExpiry) { + return allOptions; + } + + // temp account: filter options + var now = new Date(); + var tempExpiry = Twinkle.block.tempAccountExpiry; + var remainingMs = tempExpiry.getTime() - now.getTime(); + + var durationMs = { + '3 hours': 3 * 60 * 60 * 1000, + '12 hours': 12 * 60 * 60 * 1000, + '1 day': 24 * 60 * 60 * 1000, + '31 hours': 31 * 60 * 60 * 1000, + '2 days': 2 * 24 * 60 * 60 * 1000, + '3 days': 3 * 24 * 60 * 60 * 1000, + '1 week': 7 * 24 * 60 * 60 * 1000, + '2 weeks': 14 * 24 * 60 * 60 * 1000, + '1 month': 30 * 24 * 60 * 60 * 1000, + '3 months': 90 * 24 * 60 * 60 * 1000, + '6 months': 180 * 24 * 60 * 60 * 1000, + '1 year': 365 * 24 * 60 * 60 * 1000, + '2 years': 2 * 365 * 24 * 60 * 60 * 1000, + '3 years': 3 * 365 * 24 * 60 * 60 * 1000 + }; + + var filteredOptions = allOptions.filter(function(opt) { + if (opt.value === 'custom') { + return true; + } + + if (opt.value === 'infinity') { + return false; + } + + var duration = durationMs[opt.value]; + if (duration) { + return duration <= remainingMs; + } + return true; + }); + + filteredOptions.splice(1, 0, { + label: conv({ hans: '无限期(事实上)', hant: '無限期(事實上)' }), + value: 'tempaccountindefinity' + }); + + return filteredOptions; +}; + Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) { var expiry = e.target.form.expiry; if (e.target.value === 'custom') { Morebits.QuickForm.setElementVisibility(expiry.parentNode, true); + } else if (e.target.value === 'tempaccountindefinity') { + Morebits.QuickForm.setElementVisibility(expiry.parentNode, false); + expiry.value = Twinkle.block.tempAccountExpiry.toGMTString(); } else { Morebits.QuickForm.setElementVisibility(expiry.parentNode, false); expiry.value = e.target.value; } }; +Twinkle.block.callback.validateTempAccountExpiry = function twinkleblockCallbackValidateTempAccountExpiry(expiryValue) { + if (!Twinkle.block.isTempAccount || !Twinkle.block.tempAccountExpiry) { + return true; + } + + var now = new Date(); + var blockExpiryDate; + + if (Morebits.string.isInfinity(expiryValue)) { + blockExpiryDate = new Date(8640000000000000); // Max date + } else if (Date.parse(expiryValue)) { + blockExpiryDate = new Date(expiryValue); + } else { + // relative time + var durationMs = { + hours: 60 * 60 * 1000, + hour: 60 * 60 * 1000, + days: 24 * 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + weeks: 7 * 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + months: 30 * 24 * 60 * 60 * 1000, + month: 30 * 24 * 60 * 60 * 1000, + years: 365 * 24 * 60 * 60 * 1000, + year: 365 * 24 * 60 * 60 * 1000 + }; + var match = expiryValue.match(/(\d+)\s*(hours?|days?|weeks?|months?|years?)/i); + if (match) { + var num = parseInt(match[1], 10); + var unit = match[2].toLowerCase(); + blockExpiryDate = new Date(now.getTime() + (num * durationMs[unit])); + } else { + // if dount, accept + return true; + } + } + + if (blockExpiryDate > Twinkle.block.tempAccountExpiry) { + var registrationStr = Twinkle.block.tempAccountRegistration.toLocaleString(); + var expiryStr = Twinkle.block.tempAccountExpiry.toLocaleString(); + var message = conv({ + hans: '该临时账号于' + registrationStr + '创建,即于' + expiryStr + '到期,为避免不必要地占据封禁列表,建议不要设定超过到期时间的封禁期限。\n是否继续设置本期限?', + hant: '該臨時帳號於' + registrationStr + '創建,即於' + expiryStr + '到期,為避免不必要地佔據封鎖列表,建議不要設定超過到期時間的封鎖期限。\n是否繼續設置本期限?' + }); + if (!confirm(message)) { + return Twinkle.block.tempAccountExpiry.toGMTString(); + } + } + return true; +}; + Twinkle.block.seeAlsos = []; Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() { var reason = this.form.reason.value.replace( @@ -1683,18 +1812,24 @@ Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, // don't override original expiry if useInitialOptions is set if (!data.useInitialOptions) { - if (Date.parse(expiry)) { + if (Twinkle.block.isTempAccount && Twinkle.block.tempAccountExpiry && Morebits.string.isInfinity(expiry)) { + expiry = Twinkle.block.tempAccountExpiry.toGMTString(); + form.expiry_preset.value = 'tempaccountindefinity'; + form.expiry.value = expiry; + Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false); + } else if (Date.parse(expiry)) { expiry = new Date(expiry).toGMTString(); form.expiry_preset.value = 'custom'; - } else { - form.expiry_preset.value = data.expiry || 'custom'; - } - - form.expiry.value = expiry; - if (form.expiry_preset.value === 'custom') { + form.expiry.value = expiry; Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true); } else { - Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false); + form.expiry_preset.value = data.expiry || 'custom'; + form.expiry.value = expiry; + if (form.expiry_preset.value === 'custom') { + Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true); + } else { + Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false); + } } } @@ -1954,6 +2089,14 @@ Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) { } else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) { return alert(conv({ hans: '禁止无限期封禁IP地址!', hant: '禁止無限期封鎖IP位址!' })); } + + if (Twinkle.block.isTempAccount && Twinkle.block.tempAccountExpiry) { + var validationResult = Twinkle.block.callback.validateTempAccountExpiry(blockoptions.expiry); + if (validationResult !== true) { + blockoptions.expiry = validationResult; + } + } + if (!blockoptions.reason) { return alert(conv({ hans: '请提供封禁理由!', hant: '請提供封鎖理由!' })); } From d47e3e34cb6bd506eb35c65d92c156246e7ff6e6 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 5 Jan 2026 07:49:15 +0800 Subject: [PATCH 2/7] use mw library --- modules/twinkleblock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index 6bc072ce..36a80b3c 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -243,7 +243,7 @@ Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) { return e.group; }).indexOf('bot') !== -1; - Twinkle.block.isTempAccount = !!userinfo.groups && userinfo.groups.indexOf('temp') !== -1; + Twinkle.block.isTempAccount = mw.util.isTemporaryUser(relevantUserName); if (Twinkle.block.isTempAccount && userinfo.registration) { var registrationDate = new Date(userinfo.registration); Twinkle.block.tempAccountRegistration = registrationDate; From f9e3c830ed2d9c0d6e2e83ca31b0555919c69cae Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 5 Jan 2026 07:53:25 +0800 Subject: [PATCH 3/7] no need to query user.groups at init --- modules/twinkleblock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index 36a80b3c..6e599ef0 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -303,7 +303,7 @@ Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) { } else { query.bkusers = Morebits.relevantUserName(true); // groupmemberships only relevant for registered users - query.usprop = 'groupmemberships|registration|groups'; + query.usprop = 'groupmemberships|registration'; } api.get(query).then(function(data) { From d095f31c1b6f9d0cd1e8a573b78784ce5891d956 Mon Sep 17 00:00:00 2001 From: Hamish Date: Mon, 5 Jan 2026 07:54:52 +0800 Subject: [PATCH 4/7] revise comment --- modules/twinkleblock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index 6e599ef0..8d99dbc1 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -1764,7 +1764,7 @@ Twinkle.block.callback.validateTempAccountExpiry = function twinkleblockCallback var unit = match[2].toLowerCase(); blockExpiryDate = new Date(now.getTime() + (num * durationMs[unit])); } else { - // if dount, accept + // if doubt, accept return true; } } From 6884ca560a7f784e5d585f2133174e908e29fca0 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 9 Jan 2026 13:29:16 +0800 Subject: [PATCH 5/7] use morebits to display TA expiry --- modules/twinkleblock.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index 8d99dbc1..1ba5d4ac 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -449,6 +449,7 @@ Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction field_block_options = new Morebits.QuickForm.Element({ type: 'field', label: conv({ hans: '封禁选项', hant: '封鎖選項' }), name: 'field_block_options' }); field_block_options.append({ type: 'div', name: 'currentblock', label: ' ' }); field_block_options.append({ type: 'div', name: 'hasblocklog', label: ' ' }); + field_block_options.append({ type: 'div', name: 'tempaccountexpiry', label: ' ' }); field_block_options.append({ type: 'select', name: 'expiry_preset', @@ -464,14 +465,6 @@ Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry }); - if (Twinkle.block.isTempAccount && Twinkle.block.tempAccountExpiry) { - field_block_options.append({ - type: 'div', - name: 'tempaccountinfo', - label: conv({ hans: '临时账号到期时间:', hant: '臨時帳號到期時間:' }) + Twinkle.block.tempAccountExpiry.toLocaleString() - }); - } - if (partialBox) { // Partial block field_block_options.append({ type: 'select', @@ -1046,6 +1039,16 @@ Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction } else if (templateBox) { Twinkle.block.callback.change_template(e); } + + if (Twinkle.block.isTempAccount && Twinkle.block.tempAccountExpiry) { + var valid = Twinkle.block.tempAccountExpiry.getTime() - new Date().getTime(); + Morebits.Status.init($('div[name="tempaccountexpiry"] span').last()[0]); + if (valid > 0) { + Morebits.Status.info(conv({ hans: '临时账号到期时间', hant: '臨時帳號到期時間' }), Twinkle.block.tempAccountExpiry.toLocaleString()); + } else { + Morebits.Status.warn(conv({ hans: '(已过期)临时账号到期时间', hant: '(已過期)臨時帳號到期時間' }), Twinkle.block.tempAccountExpiry.toLocaleString()); + } + } }; /* From f7136b3ea1649c2971bb9a4702fc39d13287663c Mon Sep 17 00:00:00 2001 From: xiplus Date: Sat, 10 Jan 2026 11:49:37 +0800 Subject: [PATCH 6/7] Use Morebits.Date.calendar --- modules/twinkleblock.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index 1ba5d4ac..c65467c9 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -245,9 +245,9 @@ Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) { Twinkle.block.isTempAccount = mw.util.isTemporaryUser(relevantUserName); if (Twinkle.block.isTempAccount && userinfo.registration) { - var registrationDate = new Date(userinfo.registration); + var registrationDate = new Morebits.Date(userinfo.registration); Twinkle.block.tempAccountRegistration = registrationDate; - Twinkle.block.tempAccountExpiry = new Date(registrationDate.getTime() + (90 * 24 * 60 * 60 * 1000)); + Twinkle.block.tempAccountExpiry = new Morebits.Date(registrationDate.getTime() + (90 * 24 * 60 * 60 * 1000)); } else { Twinkle.block.tempAccountRegistration = null; Twinkle.block.tempAccountExpiry = null; @@ -1044,9 +1044,9 @@ Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction var valid = Twinkle.block.tempAccountExpiry.getTime() - new Date().getTime(); Morebits.Status.init($('div[name="tempaccountexpiry"] span').last()[0]); if (valid > 0) { - Morebits.Status.info(conv({ hans: '临时账号到期时间', hant: '臨時帳號到期時間' }), Twinkle.block.tempAccountExpiry.toLocaleString()); + Morebits.Status.info(conv({ hans: '临时账号到期时间', hant: '臨時帳號到期時間' }), Twinkle.block.tempAccountExpiry.calendar('utc')); } else { - Morebits.Status.warn(conv({ hans: '(已过期)临时账号到期时间', hant: '(已過期)臨時帳號到期時間' }), Twinkle.block.tempAccountExpiry.toLocaleString()); + Morebits.Status.warn(conv({ hans: '(已过期)临时账号到期时间', hant: '(已過期)臨時帳號到期時間' }), Twinkle.block.tempAccountExpiry.calendar('utc')); } } }; @@ -1773,8 +1773,8 @@ Twinkle.block.callback.validateTempAccountExpiry = function twinkleblockCallback } if (blockExpiryDate > Twinkle.block.tempAccountExpiry) { - var registrationStr = Twinkle.block.tempAccountRegistration.toLocaleString(); - var expiryStr = Twinkle.block.tempAccountExpiry.toLocaleString(); + var registrationStr = Twinkle.block.tempAccountRegistration.calendar('utc'); + var expiryStr = Twinkle.block.tempAccountExpiry.calendar('utc'); var message = conv({ hans: '该临时账号于' + registrationStr + '创建,即于' + expiryStr + '到期,为避免不必要地占据封禁列表,建议不要设定超过到期时间的封禁期限。\n是否继续设置本期限?', hant: '該臨時帳號於' + registrationStr + '創建,即於' + expiryStr + '到期,為避免不必要地佔據封鎖列表,建議不要設定超過到期時間的封鎖期限。\n是否繼續設置本期限?' From d2c8bace25de4532cd6533b14ea4c65a0c1c3b20 Mon Sep 17 00:00:00 2001 From: Hamish Date: Sun, 11 Jan 2026 12:25:51 +0800 Subject: [PATCH 7/7] manual formatTime() for TA --- modules/twinkleblock.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/twinkleblock.js b/modules/twinkleblock.js index c65467c9..497c1251 100644 --- a/modules/twinkleblock.js +++ b/modules/twinkleblock.js @@ -2417,7 +2417,12 @@ Twinkle.block.callback.closeRequest = function twinkleblockCallbackCloseRequest( var statusElement = vipPage.getStatusElement(); var userName = Morebits.relevantUserName(true); - var expiryText = Morebits.string.formatTime(params.expiry); + var expiryText; + if (Twinkle.block.tempAccountExpiry && params.expiry === Twinkle.block.tempAccountExpiry.toGMTString()) { + expiryText = conv({ hans: ' 至临时账号过期', hant: ' 至臨時帳號過期' }); + } else { + expiryText = Morebits.string.formatTime(params.expiry); + } var comment = '{{Blocked|' + (Morebits.string.isInfinity(params.expiry) ? 'indef' : expiryText) + '}}。'; var requestList = text.split(/(?=\n===.+===\s*\n)/);