Jump to content

Module:NewPagesFeed

From Wikipedia, the free encyclopedia

-- Make a link to Special:NewPagesFeed with URL parameters. ([[phab:T323488]])
-- e.g. {{#invoke:NewPagesFeed|main|feed=npp|username=Example; Example 2}}
local p = {}

local TARGET = 'Special:NewPagesFeed'
local ALLOWED = { feed = true, username = true, status = true, from = true, to = true, ores = true }
local FEEDS = { npp = true, afc = true }
local STATUSES = { reviewed = true, unreviewed = true }
-- Must stay in sync with /modules/ext.pageTriage.newPagesFeed/usernames.js
local MAX_USERNAMES = 10
local function parseUsernames( value )
	value = value:gsub( ';', '|' )
	local usernames, seen = {}, {}
	for part in mw.text.gsplit( value, '|', true ) do
		local name = mw.text.trim( ( part:gsub( '_', ' ' ) ) )
		if name ~= '' and not seen[ name ] then
			seen[ name ] = true
			usernames[ #usernames + 1 ] = name
		end
	end
	return usernames
end

-- Split a comma-separated value into trimmed, lowercased, non-empty tokens
-- (same as parseList in the JS).
local function parseList( value )
	local tokens = {}
	for part in mw.text.gsplit( value, ',', true ) do
		part = mw.ustring.lower( mw.text.trim( part ) )
		if part ~= '' then
			tokens[ #tokens + 1 ] = part
		end
	end
	return tokens
end
local function isValidDate( value )
	local y, m, d = value:match( '^(%d%d%d%d)%-(%d%d)%-(%d%d)$' )
	if not y then
		return false
	end
	y, m, d = tonumber( y ), tonumber( m ), tonumber( d )
	if y < 100 or m < 1 or m > 12 or d < 1 then
		return false
	end
	local leap = ( y % 4 == 0 and y % 100 ~= 0 ) or y % 400 == 0
	local monthDays = { 31, leap and 29 or 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
	return d <= monthDays[ m ]
end

local function formatError( messages )
	return require( 'Module:Error' ).error{
		message = 'NewPagesFeed: ' .. table.concat( messages, '; ' )
	}
end

function p.main( frame )
	local args = require( 'Module:Arguments' ).getArgs( frame )
	local errors = {}
	local query = {}

	-- Reject typos and stray arguments instead of silently dropping them
	for key in pairs( args ) do
		if type( key ) == 'string' and not ALLOWED[ key ] then
			errors[ #errors + 1 ] = 'unknown parameter "' .. key ..
				'" (if this is part of the link text and contains "=", use 1= for the label)'
		elseif type( key ) == 'number' and key ~= 1 then
			errors[ #errors + 1 ] = 'unexpected positional parameter ' .. key ..
				' (only the first, the link text, is used)'
		end
	end
	
	if args.feed then
		if FEEDS[ args.feed ] then
			query.feed = args.feed
		else
			errors[ #errors + 1 ] = 'feed must be "npp" or "afc" (got "' .. args.feed .. '")'
		end
	end

	-- username: "|"/";"-separated list
	if args.username then
		local usernames = parseUsernames( args.username )
		if #usernames == 0 then
			errors[ #errors + 1 ] = 'username must contain at least one username'
		elseif #usernames > MAX_USERNAMES then
			errors[ #errors + 1 ] = 'username accepts at most ' .. MAX_USERNAMES ..
				' distinct usernames (got ' .. #usernames .. ')'
		else
			query.username = table.concat( usernames, '|' )
		end
	end

	if args.status then
		local tokens, bad = parseList( args.status ), {}
		local good = {}
		for _, token in ipairs( tokens ) do
			if STATUSES[ token ] then
				good[ #good + 1 ] = token
			else
				bad[ #bad + 1 ] = token
			end
		end
		if #tokens == 0 or #bad > 0 then
			errors[ #errors + 1 ] = 'status must be a comma-separated list of "reviewed" and/or "unreviewed"' ..
				( #bad > 0 and ( ' (invalid: ' .. table.concat( bad, ', ' ) .. ')' ) or '' )
		else
			query.status = table.concat( good, ',' )
		end
	end

	for _, name in ipairs{ 'from', 'to' } do
		local value = args[ name ]
		if value then
			if isValidDate( value ) then
				query[ name ] = value
			else
				errors[ #errors + 1 ] = name .. ' must be a valid date in YYYY-MM-DD format (got "' .. value .. '")'
			end
		end
	end
	if query.from and query.to and query.from > query.to then
		errors[ #errors + 1 ] = 'from must not be later than to'
	end

	-- ores: comma-separated class names. The valid set isn't known here
	-- (the JS checks it against the filter form), so only require non-empty.
	if args.ores then
		local tokens = parseList( args.ores )
		if #tokens == 0 then
			errors[ #errors + 1 ] = 'ores must be a comma-separated list of class names'
		else
			query.ores = table.concat( tokens, ',' )
		end
	end

	if #errors > 0 then
		return formatError( errors )
	end

	local url = mw.title.new( TARGET ):fullUrl( query, 'canonical' )

	-- "]" and newlines would terminate an external link's label early
	local label = args[ 1 ] or TARGET
	label = label:gsub( '%]', '&#93;' ):gsub( '[\r\n]+', ' ' )

	-- plainlinks suppresses the external-link icon so it looks like a wikilink
	return '<span class="plainlinks">[' .. url .. ' ' .. label .. ']</span>'
end

return p