Module:RFX

-- --                         Module:RFX                              -- -- This is a library for retrieving information about Requests     -- -- for Stewardship, Requests for global rights and every other     -- -- request pages that has Support, Abstain and Oppose sections     -- --

local libraryUtil = require('libraryUtil') local lang = mw.getContentLanguage local textSplit = mw.text.split local umatch = mw.ustring.match local newTitle = mw.title.new

local rfx = {}

-- --        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 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 userStart, userEnd, userMatch = vote:find('%[%[[%s_]*[uU][sS][eE][rR][%s_]*:[%s_]*(.-)[%s_]*%]%].-$')	local talkStart, talkEnd, talkMatch = vote:find('%[%[[%s_]*[uU][sS][eE][rR][%s_]+[tT][aA][lL][kK][%s_]*:[%s_]*(.-)[%s_]*%]%].-$')	local username	if userStart and talkStart then		if userStart > talkStart then			username = userMatch		else			username = talkMatch		end	elseif userStart then		username = userMatch	elseif talkStart then		username = talkMatch	else		return string.format( "Error parsing signature: %s", vote )	end	username = username:match('^[^|/#]*')	return username 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 = {...} for i, usernames in ipairs(tables) do		for j, username in ipairs(usernames) do			username = lang:ucfirst(username) if exists[username] then return true else exists[username] = true end end end return false end

-- --  Define the constructor function    -- --

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 on RFGR, RFS or RFC. title = getTitleObject(title) if not title then return nil end function data:getTitleObject checkSelf(self, 'getTitleObject') return title end if title.namespace == 0 then local rootText = title.rootText if rootText == 'Requests for global rights' then data.type = 'rfgr' elseif rootText == 'Requests for Stewardship' then data.type = 'rfs' elseif rootText == 'Requests for Comment' then data.type = 'rfc' else return nil 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 local introText, supportText, abstainText, opposeText = umatch(		pageText,		'^(.-)\n==[^=\n][^\n]-==.-'		.. '\n===%s*[sS]upport%s*===(.-)'		.. '\n===%s*[aA]bstain%s*===(.-)'		.. '\n===%s*[oO]ppose%s*===(.-)$'	) if not introText then introText, supportText, abstainText, opposeText = umatch(			pageText,			"^(.-\n[^\n]-%(%d+/%d+/%d+%)[^\n]-)\n.-"			.. "\nSupport(.-)\nAbstain(.-)\nOppose(.-)"		) end

-- Get vote counts. local supportVotes, abstainVotes, opposeVotes if supportText and abstainText and opposeText then supportVotes = parseVoteBoundaries(supportText) abstainVotes = parseVoteBoundaries(abstainText) opposeVotes = parseVoteBoundaries(opposeText) end local supports, abstains, opposes if supportVotes and abstainVotes and opposeVotes then supports = #supportVotes data.supports = supports abstains = #abstainVotes data.abstains = abstains opposes = #opposeVotes data.opposes = opposes 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:getAbstainUsers checkSelf(self, 'getAbstainUsers') if abstainVotes then return parseVoters(abstainVotes) else return nil end end

function data:getOpposeUsers checkSelf(self, 'getOpposeUsers') if opposeVotes then return parseVoters(opposeVotes) else return nil end end

function data:dupesExist checkSelf(self, 'dupesExist') local supportUsers = self:getSupportUsers local abstainUsers = self:getAbstainUsers local opposeUsers = self:getOpposeUsers if not (supportUsers and abstainUsers and opposeUsers) then return nil end return dupesExist(supportUsers, abstainUsers, opposeUsers) 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 if introText then data.endTime = umatch(introText, '(%d%d:%d%d, %d+ %w+ %d+) %(UTC%)') data.user = umatch(introText, '==%s*%[%[[_%s]*[pP]roject[_%s]*:[_%s]*[rR]equests[_ ]for[_ ]%w+/.-|[_%s]*(.-)[_%s]*%]%][_%s]*==')		if not data.user then			data.user = umatch(introText, '==%s*([^\n]-)%s*==')		end	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 now = tonumber(lang:formatDate("U"))		local success, endTimeU = pcall(lang.formatDate, lang, 'U', endTime)		if not success then			return nil		end		endTimeU = tonumber(endTimeU)		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:getReport -- Gets the URI object for Vote History tool checkSelf(self, 'getReport') return mw.uri.new('//apersonbot.toolforge.org/vote-history?page=' .. 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' ) local rfxType = data.type if rfxType == 'rfgr' then if umatch(				pageText,				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for global rights(.-)[%s_]*%]%]'			) then				return 'successful'			elseif umatch(				pageText,				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[uU]nsuccessful requests for global rights(.-)[%s_]*%]%]'			) then				return 'unsuccessful'			end		elseif rfxType == 'rfs' then			if umatch(				pageText,				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for stewardship(.-)[%s_]*%]%]'			) then				return 'successful'			elseif umatch(				pageText,				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[uU]nsuccessful requests for stewardship(.-)[%s_]*%]%]'			) then				return 'unsuccessful'			end		elseif rfxType == 'rfc' then			if umatch(				pageText,				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[sS]uccessful requests for comment(.-)[%s_]*%]%]'			) then				return 'successful'			elseif umatch(				pageText,				'%[%[[%s_]*[cC][aA][tT][eE][gG][oO][rR][yY][%s_]*:[%s_]*[uU]nsuccessful requests for comment(.-)[%s_]*%]%]'			) then				return 'unsuccessful'			end		end		local secondsLeft = self:getSecondsLeft		if secondsLeft and secondsLeft > 0 then			return 'open'		elseif secondsLeft and secondsLeft <= 0 then			return 'pending closure'		else			return nil		end	end	-- Specify which fields are read-only, and prepare the metatable.	local readOnlyFields = {		getTitleObject = true,		['type'] = true,		getSupportUsers = true,		getAbstainUsers = true,		getOpposeUsers = true,		supports = true,		abstains = true,		opposes = true,		endTime = true,		percent = true,		user = true,		dupesExist = true,		getSecondsLeft = true,		getTimeLeft = true,		getReport = true,		getStatus = 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

return setmetatable( obj, {		__pairs = function ( t )			return pairsfunc, t, nil		end,		__index = data,		__newindex = function( t, key, value )			if readOnlyFields[ key ] then				error( 'index "' .. key .. '" is read-only', 2 )			else				rawset( t, key, value )			end		end,		__tostring = function( t )			return t:getTitleObject.prefixedText		end	} ) end

return rfx