Module: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 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, userMatch = vote:match('^(.*)%[%[[%s_]*:?[%s_]*[uU][sS][eE][rR][%s_]*:[%s_]*(.-)[%s_]*%]%].-$')

local talkStart, talkMatch = vote:match('^(.*)%[%[[%s_]*:?[%s_]*[uU][sS][eE][rR][%s_]+[tT][aA][lL][kK][%s_]*:[%s_]*(.-)[%s_]*%]%].-$')

local contribStart, contribMatch = vote:match('^(.*)%[%[[%s_]*:?[%s_]*[sS][pP][eE][cC][iI][aA][lL][%s_]*:[%s_]*[cC][oO][nN][tT][rR][iI][bB][uU][tT][iI][oO][nN][sS]/[%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

elseif contribStart then

username = contribMatch

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

local function hasCategory(category, catList)

for _, c in ipairs(catList) do

if c == category then

return true

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 a subpage of WP:RFA or WP:RFB.

title = getTitleObject(title)

if not title then

return nil

end

function data:getTitleObject()

checkSelf(self, 'getTitleObject')

return title

end

if title.namespace == 4 then

local rootText = title.rootText

if rootText == 'Requests for adminship' then

data.type = 'rfa'

elseif rootText == 'Requests for bureaucratship' then

data.type = 'rfb'

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, opposeText, neutralText = umatch(

pageText,

'^(.-)\n====[^=\n][^\n]-====.-'

.. '\n=====%s*[sS]upport%s*=====(.-)'

.. '\n=====%s*[oO]ppose%s*=====(.-)'

.. '\n=====%s*[nN]eutral%s*=====(.-)$'

)

if not introText then

introText, supportText, opposeText, neutralText = umatch(

pageText,

"^(.-\n[^\n]-%(%d+/%d+/%d+%)[^\n]-)\n.-"

.. "\nSupport(.-)\nOppose(.-)\nNeutral(.-)"

)

end

-- Switch to reconfirmation request for adminship if in that category

local categories = title.categories

if hasCategory('Reconfirmation requests for adminship', categories) then

data.type = 'rrfa'

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 supports, opposes, neutrals

if supportVotes and opposeVotes and neutralVotes then

supports = #supportVotes

data.supports = supports

opposes = #opposeVotes

data.opposes = opposes

neutrals = #neutralVotes

data.neutrals = neutrals

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

if introText then

data.endTime = umatch(introText, '(%d%d:%d%d, %d+ %w+ %d+) %(UTC%)')

data.user = umatch(introText, '===%s*%[%[[_%s]*[wW]ikipedia[_%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('https://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 == 'rfa' or rfxType == 'rrfa' then

if hasCategory('Successful requests for adminship', categories) then

return 'successful'

elseif hasCategory('Unsuccessful requests for adminship', categories) then

return 'unsuccessful'

end

elseif rfxType == 'rfb' then

if hasCategory('Successful requests for bureaucratship', categories) then

return 'successful'

elseif hasCategory('Unsuccessful requests for bureaucratship', categories) 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,

getOpposeUsers = true,

getNeutralUsers = true,

supports = true,

opposes = true,

neutrals = 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