模組:Rfx
外觀
![]() | 此模組已評為alpha版,可接受第三方輸入,並可用於少量頁面以檢查是否存在問題,但需要受到檢查。歡迎提供新功能或修改其輸入輸出機制的建議。 |
此模塊可以解析申請成為管理人員(RfX)投票的信息,目前支持管理員、行政員、用戶查核員、監督員和界面管理員投票。通常此模塊不應直接被內容頁面調用,而應作為底層模塊被其他Lua模塊調用。
構造rfx對象
[編輯]首先必須加載此模塊:
local rfx = require( 'Module:Rfx' )
之後調用rfx.new()
構造RfX對象。注意:高開銷函數(見下)
rfx.new()
調用方法如下:
local myRfx = rfx.new( pagename )
pagename
需為完整的RfX頁面名,例如:
local exampleRfa = rfx.new( 'Wikipedia:申请成为管理员/Example' )
如果未指定pagename
,或它並不是WP:申請成為管理員、WP:申請成為行政員、WP:申請成為用戶查核員、WP:申請成為監督員或WP:申請成為界面管理員的子頁面,則rfx.new
會返回nil
。
rfx對象的屬性與方法
[編輯]構造成功rfx
對象後,就可以使用以下屬性與方法了。注意這些屬性與方法均只讀。
- 屬性
type
:RfX的類型。可以為「rfa
」、「rfb
」、「rfcu
」、「rfo
」或「rfia
」。supports
:支持數。如果無法解析投票數,返回nil
。opposes
:反對數。如果無法解析投票數,返回nil
。neutrals
:中立數。如果無法解析投票數,返回nil
。percent
:支持率,即,四捨五入至整數。如果無法解析投票數,返回nil
。endTime
:結束時間,通過抓取RfX頁面的源代碼得到,為字符串類型。如果無法解析,返回nil
。user
:候選人,不帶「User:」前綴。如果無法解析,返回nil
。
- 方法
請用下述格式調用以下方法:
local titleObject = exampleRfa:getTitleObject()
getTitleObject()
:返回RfX頁面的標題對象,參見Lua手冊。getSupportUsers()
:返回一張表,為所有投支持票的用戶,根據簽名判斷。如果無法解析某個簽名,則對應值為「簽名剖析失敗:(該用戶的投票源代碼)」。該表可傳遞給dupesExist()
函數判斷是否有重複投票。如果完全無法解析支持票所在段落,返回nil
。getOpposeUsers()
:類似於getSupportUsers()
,返回投反對票的用戶列表。getNeutralUsers()
:類似於getSupportUsers()
,返回投中立票的用戶列表。dupesExist()
:返回一張表,為所有重複投票的用戶。如果沒有重複投票的用戶,返回值是空的表。如果無法解析傳入參數,返回nil
。getSecondsLeft()
:返回距離投票結束的秒數。若投票已經結束,則返回0。如果無法解析結束時間,返回nil
。getTimeLeft()
:返回距離投票結束的時間,為一字符串,格式為「x days, y hours
」。如果已經結束或無法解析,返回nil
。getReport()
:返回Jimmy的驗票工具對應的URI對象。getStatus()
:返回RfX的當前狀態,為「投票中」或「已結束」。如果無法解析,返回nil
。isSecurePoll()
:返回RfX是不是使用安全投票,參見Module:Rfx/correction。
另外,rfx
對象重載了==
運算符。如果兩個rfx對象對應頁面相同,則返回true。tostring( rfx )
會返回RfX頁面對象的prefixedTitle
屬性(參見Lua手冊)。
高開銷函數
[編輯]為獲取RfX頁面的源代碼,此模塊使用了title:getContent方法。每次建立rfx對象時都會調用一次該函數,所以調用rfx.new
會被計入高開銷解析函數調用數。所以請注意,如果當前的RfX數目太多,這個模塊可能會運行異常(當前限制為每個頁面最多調用500次高開銷解析函數)。此外,在使用本模塊的頁面的Special:鏈入頁面中,可以看到對應RfX頁面被嵌入包含。
----------------------------------------------------------------------
-- Module:Rfx --
-- This is a library for retrieving information about requests --
-- for adminship and requests for bureaucratship on the English --
-- Wikipedia. Please see the module documentation for instructions. --
----------------------------------------------------------------------
local libraryUtil = require('libraryUtil')
local lang = mw.getContentLanguage()
local textSplit = mw.text.split
local umatch = mw.ustring.match
local newTitle = mw.title.new
local validSignPrefixes = {
['u']=1, ['user']=1, ['用户']=1, ['用戶']=1,
['ut']=1, ['user talk']=1, ['用户讨论']=1, ['用戶討論']=1,
['special:contribs']=1, ['特殊:contribs']=1,
['special:contributions']=1, ['特殊:contributions']=1,
['特殊:用户贡献']=1, ['特殊:用戶貢獻']=1,
['special:用户贡献']=1, ['special:用戶貢獻']=1
}
local rfx = {}
local corrections = require('Module:Rfx/correction')
local mVar = require('Module:Var')
--------------------------------------
-- Helper functions --
--------------------------------------
local function getTitleObject(title)
local success, titleObject = pcall(newTitle, title)
if success and titleObject then
return titleObject
else
return nil
end
end
local function parseVoteBoundaries(section)
-- Returns an array containing the raw wikitext of RfX votes in a given section.
section = section:match('^.-\n#(.*)$') -- Strip non-votes from the start.
if not section then
return {}
end
-- WhitePhosphorus: Do not discard anything, or we may lose votes.
--- See [[special:permalink/45633636]].
-- section = section:match('^(.-)\n[^#]') or section -- Discard subsequent numbered lists.
local comments = textSplit(section, '\n#')
local votes = {}
for i, comment in ipairs(comments) do
if comment:find('^[^#*;:].*%S') then
votes[#votes + 1] = comment
end
end
return votes
end
local function parseVote(vote)
-- parses a username from an RfX vote.
local b, e, link, username, page, colon, slash, prefix = nil, 0
while true do
-- extract all links
b, e, link = vote:find('%[%[[_%s]*:?[_%s]*(.-)[_%s]*%]%]', e+1)
if not link then
break
end
-- some strange links like User__ ___talk:_ __ Example is also valid.
link = link:gsub('|.*', ''):gsub('[%s_]+', ' '):gsub('([:/]) ', '%1')
colon, slash = link:find('/'), link:find(':')
if colon then
prefix = link:sub(1, colon-1):lower()
if validSignPrefixes[prefix] then
username = link:sub(colon+1)
end
end
if slash then
prefix = link:sub(1, slash-1):lower()
if validSignPrefixes[prefix] then
username = link:sub(slash+1)
end
end
end
if not username then
return string.format( "'''签名-{zh-cn:解析;zh-tw:剖析}-失败''':''%s''", vote )
end
return username:match('^[^/#]*')
end
local function parseVoters(votes)
local voters = {}
for i, vote in ipairs(votes) do
voters[#voters + 1] = parseVote(vote)
end
return voters
end
local function dupesExist(...)
local exists = {}
local tables = {...}
local dupes = {}
for i, usernames in ipairs(tables) do
for j, username in ipairs(usernames) do
username = lang:ucfirst(username)
if exists[username] then
dupes[username] = true
else
exists[username] = true
end
end
end
local result = {}
for username, _ in pairs(dupes) do
table.insert(result, username)
end
return result
end
local function safeFormatDate(format, timestamp, langObj)
langObj = langObj or mw.language.getContentLanguage()
local success, result = pcall(function ()
return langObj:formatDate(format, timestamp)
end)
return success and result or nil
end
local now = tonumber(safeFormatDate("U"))
local function fetchTimesFromMVar(frame, title)
frame:expandTemplate({title = title, args = {}})
local vars = mVar._getVars('VAR')
mVar.makeVar({'VoteState-Start', 'VoteState-End'}, 'DELETE')
local rawStartTime = vars['VoteState-Start'] ~= nil and vars['VoteState-Start'].value or nil
local rawEndTime = vars['VoteState-End'] ~= nil and vars['VoteState-End'].value or nil
rawStartTime = rawStartTime ~= '' and rawStartTime or nil
rawEndTime = rawEndTime ~= '' and rawEndTime or nil
return rawStartTime, rawStartTime and safeFormatDate('Y-m-d\\TH:i:s\\Z', rawStartTime),
rawEndTime, rawEndTime and safeFormatDate('Y-m-d\\TH:i:s\\Z', rawEndTime)
end
------------------------------------------
-- Define the constructor function --
------------------------------------------
local constructCache = {}
function rfx.new(title)
local obj = {}
local data = {}
local checkSelf = libraryUtil.makeCheckSelfFunction( 'Module:Rfx', 'rfx', obj, 'rfx object' )
-- Get the title object and check to see whether we are a subpage of WP:RFA or WP:RFB.
title_bak = title
title = getTitleObject(title)
if not title then
return nil
end
-- 如果緩存有結果先抽緩存的,減少重複調用高開銷函數
if constructCache[ title.prefixedText ] then
return constructCache[ title.prefixedText ]
end
function data:getTitleObject()
checkSelf(self, 'getTitleObject')
return title
end
if title.namespace == 4 then
local rootText = title.rootText
if rootText == '申请成为管理员' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申请成为管理员/', '申請成為管理員/'))
end
data.type = 'rfa'
elseif rootText == '申請成為管理員' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申請成為管理員/', '申请成为管理员/'))
end
data.type = 'rfa'
elseif rootText == '申请成为行政员' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申请成为行政员/', '申請成為行政員/'))
end
data.type = 'rfb'
elseif rootText == '申請成為行政員' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申請成為行政員/', '申请成为行政员/'))
end
data.type = 'rfb'
elseif rootText == '申请成为用户查核员' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申请成为用户查核员/', '申請成為用戶查核員/'))
end
data.type = 'rfcu'
elseif rootText == '申請成為用戶查核員' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申請成為用戶查核員/', '申请成为用户查核员/'))
end
data.type = 'rfcu'
elseif rootText == '申请成为监督员' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申请成为监督员/', '申請成為監督員/'))
end
data.type = 'rfo'
elseif rootText == '申請成為監督員' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申請成為監督員/', '申请成为监督员/'))
end
data.type = 'rfo'
elseif rootText == '申请成为界面管理员' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申请成为界面管理员/', '申請成為介面管理員/'))
end
data.type = 'rfia'
elseif rootText == '申請成為介面管理員' then
if not title.exists then
title = getTitleObject(title_bak:gsub('申請成為介面管理員/', '申请成为界面管理员/'))
end
data.type = 'rfia'
else
return nil
end
local n = umatch(title.subpageText, '^第(%d+)次$')
if n ~= nil then
data.attempt = n
else
data.attempt = '1'
end
else
return nil
end
-- Get the page content and divide it into sections.
local pageText = title:getContent()
if not pageText then
return nil
end
frame = mw.getCurrentFrame()
pageText = string.gsub(pageText, "{{%s*[Ff]ollow[Ll]ast[Ii]ndent|(.*)%s*}}",
function (s)
return frame:expandTemplate{ title = 'FollowLastIndent', args = { s } }
end
)
-- FIXME: 反对?
--- 其实这个不太重要,毕竟大家都用RfA模版,后者生成出来是繁体的。
local introText = umatch(
pageText,
'^(.-)\n====='
)
local supportText, opposeText, neutralText = umatch(
pageText,
'=====%s*支持%s*=====(.-)'
.. '\n=====%s*反對%s*=====(.-)'
.. '\n=====%s*中立%s*=====(.-)'
.. '\n=====%s*意見%s*=====.*'
)
if not supportText then
supportText, opposeText, neutralText = umatch(
pageText,
"\n'''支持'''(.-)\n'''反對'''(.-)\n'''中立'''(.-)'''意見'''.*"
)
end
-- Get vote counts.
local supportVotes, opposeVotes, neutralVotes
if supportText and opposeText and neutralText then
supportVotes = parseVoteBoundaries(supportText)
opposeVotes = parseVoteBoundaries(opposeText)
neutralVotes = parseVoteBoundaries(neutralText)
end
local isSecurePoll = false
local correction = corrections[title.text] or {0, 0, 0}
if type(correction) == type('') then
correction = correction:lower()
if correction == 'securepoll' then
isSecurePoll = true
correction = {0, 0, 0} -- fallback
else
error('bad vaule for [[Module:Rfx/correction]]#[\'' .. title.text .. '\'] (table or \'securepoll\' expected, got \'' .. correction .. '\')')
end
end
local supports, opposes, neutrals
if supportVotes and opposeVotes and neutralVotes then
supports = #supportVotes + correction[1]
data.supports = math.max(supports, 0)
opposes = #opposeVotes + correction[2]
data.opposes = math.max(opposes, 0)
neutrals = #neutralVotes + correction[3]
data.neutrals = math.max(neutrals, 0)
end
-- Voter methods and dupe check.
function data:getSupportUsers()
checkSelf(self, 'getSupportUsers')
if supportVotes then
return parseVoters(supportVotes)
else
return nil
end
end
function data:getOpposeUsers()
checkSelf(self, 'getOpposeUsers')
if opposeVotes then
return parseVoters(opposeVotes)
else
return nil
end
end
function data:getNeutralUsers()
checkSelf(self, 'getNeutralUsers')
if neutralVotes then
return parseVoters(neutralVotes)
else
return nil
end
end
function data:dupesExist()
checkSelf(self, 'dupesExist')
local supportUsers = self:getSupportUsers()
local opposeUsers = self:getOpposeUsers()
local neutralUsers = self:getNeutralUsers()
if not (supportUsers and opposeUsers and neutralUsers) then
return nil
end
return dupesExist(supportUsers, opposeUsers, neutralUsers)
end
if supports and opposes then
local total = supports + opposes
if total <= 0 then
data.percent = 0
else
data.percent = math.floor((supports / total * 100) + 0.5)
end
end
-- 先試看看從[[Template:VoteState]]抽取起始與截止時間
-- 以前的抽取方式可能抽到非預期的時間戳
local rawStartTime, startTime, rawEndTime, endTime = fetchTimesFromMVar(frame, title)
if startTime then
data.rawStartTime = rawStartTime
data.startTime = startTime
end
if rawEndTime then
data.rawEndTime = rawEndTime
data.endTime = endTime
end
if introText then
if not data.endTime then
data.rawEndTime = umatch(introText, '%d+年%d+月%d+日%s*%([日一二三四五六]%)%s*%d+:%d+ %(UTC%)')
if data.rawEndTime then
local Y, n, j, m, s = umatch(data.rawEndTime, '(%d+)年(%d+)月(%d+)日%s*%([日一二三四五六]%)%s*(%d+):(%d+) %(UTC%)')
data.endTime = string.format('%04d-%02d-%02dT%02d:%02dZ', Y, n, j, m, s)
end
end
-- ==== [[User:Example|Nickname]] ====
data.user = umatch(introText, '====%s*%[%[[_%s]*[uU]ser[_%s]*:[_%s]*([^\n]-)|[^\n]-%]%]%s*====') or
-- ==== [[U:Example|Nickname]] ====
umatch(introText, '====%s*%[%[[_%s]*[uU][_%s]*:[_%s]*([^\n]-)|[^\n]-%]%]%s*====') or
-- ==== [[用户:Example|Nickname]] ====
umatch(introText, '====%s*%[%[[_%s]*用户[_%s]*:[_%s]*([^\n]-)|[^\n]-%]%]%s*====') or
-- ==== [[用戶:Example|Nickname]] ====
umatch(introText, '====%s*%[%[[_%s]*用戶[_%s]*:[_%s]*([^\n]-)|[^\n]-%]%]%s*====') or
-- ==== [[User:Example]] ====
umatch(introText, '====%s*%[%[[_%s]*[uU]ser[_%s]*:[_%s]*([^\n]-)%]%]%s*====') or
-- ==== [[U:Example]] ====
umatch(introText, '====%s*%[%[[_%s]*[uU][_%s]*:[_%s]*([^\n]-)%]%]%s*====') or
-- ==== [[用户:Example]] ====
umatch(introText, '====%s*%[%[[_%s]*用户[_%s]*:[_%s]*([^\n]-)%]%]%s*====') or
-- ==== [[用戶:Example]] ====
umatch(introText, '====%s*%[%[[_%s]*用戶[_%s]*:[_%s]*([^\n]-)%]%]%s*====') or
-- ==== User:Example ====
umatch(introText, '====%s*[uU]ser[_%s]*:[_%s]*([^\n]-)%s*====') or
-- ==== U:Example ====
umatch(introText, '====%s*[uU][_%s]*:[_%s]*([^\n]-)%s*====') or
-- ==== 用户:Example ====
umatch(introText, '====%s*用户[_%s]*:[_%s]*([^\n]-)%s*====') or
-- ==== 用戶:Example ====
umatch(introText, '====%s*用戶[_%s]*:[_%s]*([^\n]-)%s*====') or
-- ==== Example ====
umatch(introText, '====%s*([^\n]-)%s*====')
end
-- Methods for seconds left and time left.
function data:getSecondsLeft()
checkSelf(self, 'getSecondsLeft')
local endTime = self.endTime
if not endTime then
return nil
end
local endTimeU = tonumber(safeFormatDate('U', endTime))
if not endTimeU then
return nil
end
local secondsLeft = endTimeU - now
if secondsLeft <= 0 then
return 0
else
return secondsLeft
end
end
function data:getTimeLeft()
checkSelf(self, 'getTimeLeft')
local secondsLeft = self:getSecondsLeft()
if not secondsLeft then
return nil
end
return mw.ustring.gsub(lang:formatDuration(secondsLeft, {'days', 'hours'}), ' and', ',')
end
function data:voteIsStart()
checkSelf(self, 'voteIsStart')
if not isSecurePoll then
-- 只有安全投票才需要考慮開始了沒
return true
end
local startTime = self.startTime
if not startTime then
-- return true -- 拿不到開始日期,當作已經開始了
return false -- XXX: 安全投票下拿不到開始日期,一般是未開始
end
local startTimeU = tonumber(safeFormatDate('U', startTime))
if not startTimeU then
-- return true -- 拿不到開始日期,當作已經開始了
return false -- XXX: 安全投票下拿不到開始日期,一般是未開始
end
return now >= startTimeU
end
function data:getReport()
-- Gets the URI object for Jimmy's RfA Analysis tool
checkSelf(self, 'getReport')
if isSecurePoll then
return nil
else return mw.uri.new('//jimmy.toolforge.org/cgi-bin/rfa.py?title=' .. mw.uri.encode(title.prefixedText))
end
end
function data:getStatus()
-- Gets the current status of the RfX. Returns either "successful", "unsuccessful",
-- "open", or "pending closure". Returns nil if the status could not be found.
checkSelf( self, 'getStatus' )
-- 中文维基百科的RfX并没有有效判断成功与失败的方法,只能判断结束与否。
if not self:voteIsStart() then
return '尚未開始'
end
local secondsLeft = self:getSecondsLeft()
if secondsLeft and secondsLeft > 0 then
return '投票中'
elseif secondsLeft and secondsLeft <= 0 then
return '已结束'
else
return nil
end
end
function data:isSecurePoll()
checkSelf(self, 'isSecurePoll')
return isSecurePoll
end
-- Specify which fields are read-only, and prepare the metatable.
local readOnlyFields = {
getTitleObject = true,
['type'] = true,
getSupportUsers = true,
getOpposeUsers = true,
getNeutralUsers = true,
supports = true,
opposes = true,
neutrals = true,
endTime = true,
rawEndTime = true,
percent = true,
user = true,
dupesExist = true,
getSecondsLeft = true,
getTimeLeft = true,
getReport = true,
getStatus = true,
isSecurePoll = true
}
local function pairsfunc( t, k )
local v
repeat
k = next( readOnlyFields, k )
if k == nil then
return nil
end
v = t[k]
until v ~= nil
return k, v
end
result = setmetatable( obj, {
__pairs = function ( t )
return pairsfunc, t, nil
end,
__index = data,
__newindex = function( t, key, value )
if readOnlyFields[ key ] then
error( '下标"' .. key .. '"只读', 2 )
else
rawset( t, key, value )
end
end,
__tostring = function( t )
return t:getTitleObject().prefixedText
end
} )
-- 緩存結果
constructCache[ title.prefixedText ] = result
return result
end
return rfx