Module:NewPagesFeed
Appearance
| This module is rated as alpha. It is ready for limited use and third-party feedback. It may be used on a small number of pages, but should be monitored closely. Suggestions for new features or adjustments to input and output are welcome. |
Usage
[edit]{{#invoke:NewPagesFeed|main|params}}
Parameters
[edit]|feed=can be "afc", "npp", or blank (defaults to the user's save preference). Controls the feed shown in Special:NewPagesFeed - examples: afc feed, npp feed, no feed|username=is optionally a list of one or more usernames seperated by a semicolon or pipe - examples: Example 1, Example 1|Example 2, Example 1;Example2|ores=is optionally a a comma-separated list of ORES article qualities:featured, good, b, c, start, stub- examples: featured, featured,good|status=is optionally a comma-separated list of composed ofreviewedorunreviewed- examples: reviewed,unreviewed, reviewed|from=and|to=are optionally a date of the formatYYYY-MM-DD, controlling the date range options - examples: from 2025-08-31, from 2025-08-31 to 2026-08-31, to 2026-08-31|1=is an optionally a "piped link" equivalent.
-- 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( '%]', ']' ):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