文档图示 模块文档[查看] [编辑] [历史] [清除缓存]

此模块可以解析申请成为管理人员(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 -- 拿不到開始日期,當作已經開始了
		end
		local startTimeU = tonumber(safeFormatDate('U', startTime))
		if not startTimeU then
			return true -- 拿不到開始日期,當作已經開始了
		end
		return now >= startTimeU 
	end
	
	function data:getReport()
		-- Gets the URI object for Jimmy's RfA Analysis tool
		checkSelf(self, 'getReport')
		return isSecurePoll and nil or mw.uri.new('//jimmy.toolforge.org/cgi-bin/rfa.py?title=' .. mw.uri.encode(title.prefixedText))
	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