# Module:UserLinks

---
-- UserLinks --  
-- This module creates a list of links about a given user. It can be used on --  
-- its own or from a template. See the /doc page for more documentation. --  
---

-- Require necessary modules  
local yesno = require('Module:Yesno')  

-- Lazily initialise modules that we might or might not need  
local mExtra -- Module:UserLinks/extra  
local mArguments -- Module:Arguments  
local mToolbar -- Module:Toolbar  
local mCategoryHandler -- Module:Category handler  
local mTableTools -- Module:TableTools  
local interwikiTable -- Module:InterwikiTable, loaded with mw.loadData  

-- Load shared helper functions  
local mShared = require('Module:UserLinks/shared')  
local raiseError = mShared.raiseError  
local maybeLoadModule = mShared.maybeLoadModule  
local makeWikitextError = mShared.makeWikitextError  
local makeWikilink = mShared.makeWikilink  
local makeUrlLink = mShared.makeUrlLink  
local makeFullUrlLink = mShared.makeFullUrlLink  
local message = mShared.message  

local p = {}  

---
-- Link table  
---

function p.getLinks(snippets)  
--[=[  
-- Get a table of links that can be indexed with link codes. The table  
-- returned is blank, but links are added to it on demand when it is  
-- indexed. This is made possible by the metatable and by the various link  
-- functions, some of which are defined here, and some of which are defined  
-- at Module:UserLinks/extra.  
--]=]  
local links, linkFunctions = {}, {}  

---
-- Link functions  
---
-- The following functions make the links from the link codes and the user  
-- data snippets. New link functions should be added below the existing  
-- functions.  
---

function linkFunctions.u(snippets)  
-- User page  
return makeWikilink(  
snippets.interwiki,  
2,  
snippets.username,  
snippets.username  
end  

function linkFunctions.np(snippets)  
-- User page (no ping)  
return '<span>' .. makeFullUrlLink(  
snippets.interwiki,  
2,  
snippets.username,  
'',  
snippets.username  
) .. '</span>'  
end  

function linkFunctions.t(snippets)  
-- User talk page  
return makeWikilink(  
snippets.interwiki,  
3,  
snippets.username,  
message('display-talk')  
end  

function linkFunctions.c(snippets)  
-- Contributions  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Contribs/' .. snippets.username,  
message('display-contributions')  
end  

function linkFunctions.c64(snippets)  
-- Contributions  
local first64 = snippets.username:match('^%x+:%x+:%x+:%x+:')  
or snippets.username:match('^%x+:%x+:%x+:')  
or snippets.username:match('^%x+:%x+:')  
or snippets.username:match('^%x+:')  
return first64 and makeWikilink(  
snippets.interwiki,  
-1,  
'Contribs/' .. first64 .. ':/64',  
'(/64)'  
) or ''  
end  

function linkFunctions.ct(snippets)  
-- Edit count  
return makeUrlLink(  
{  
host = 'xtools.wmflabs.org',  
path = '/ec/',  
query = {  
username = snippets.username,  
project = snippets.toolLang .. '.' .. snippets.projectLong .. '.org'  
}  
},  
message('display-count')  
end  

function linkFunctions.m(snippets)  
-- Page moves  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/move/' .. snippets.username,  
message('display-moves')  
end  

function linkFunctions.l(snippets)  
-- Logs  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/' .. snippets.username,  
message('display-logs')  
end  

function linkFunctions.ae(snippets)  
-- Automated edits (and non-automated contributions).  
return makeUrlLink(  
{  
host = 'xtools.wmflabs.org',  
path = '/autoedits/',  
query = {  
username = snippets.username,  
project = snippets.toolLang .. '.' .. snippets.projectLong .. '.org'  
}  
},  
message('display-autoedits')  
end  

function linkFunctions.bl(snippets)  
-- Block log  
return makeFullUrlLink(  
snippets.interwiki,  
-1,  
'Log/block',  
{page = 'User:' .. snippets.username},  
message('display-blocklog')  
end  

function linkFunctions.bls(snippets)  
-- Blocks  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/block/' .. snippets.username,  
message('display-blocks')  
end  

function linkFunctions.bu(snippets)  
-- Block user  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Block/' .. snippets.username,  
message('display-blockuser')  
end  

function linkFunctions.ca(snippets)  
-- Central auth  
return makeWikilink(  
snippets.interwiki,  
-1,  
'CentralAuth/' .. snippets.username,  
message('display-centralauth')  
end  

function linkFunctions.dc(snippets)  
-- Deleted contribs  
return makeWikilink(  
snippets.interwiki,  
-1,  
'DeletedContributions/' .. snippets.username,  
message('display-deletedcontributions')  
end  

function linkFunctions.e(snippets)  
-- Email  
return makeWikilink(  
snippets.interwiki,  
-1,  
'EmailUser/' .. snippets.username,  
message('display-email')  
end  

function linkFunctions.es(snippets)  
-- Edit summaries  
return makeUrlLink(  
{  
host = 'xtools.wmflabs.org',  
path = '/editsummary/',  
query = {  
username = snippets.username,  
project = snippets.toolLang .. '.' .. snippets.projectLong .. '.org'  
}  
},  
message('display-editsummaries')  
end  

function linkFunctions.del(snippets)  
-- Deletions  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/delete/' .. snippets.username,  
message('display-deletions')  
end  

function linkFunctions.lu(snippets)  
-- List user  
return makeFullUrlLink(  
snippets.interwiki,  
-1,  
'ListUsers',  
{limit = 1, username = snippets.username},  
message('display-listuser')  
end  

function linkFunctions.sul(snippets)  
-- SUL  
return makeWikilink(  
nil,  
nil,  
'sulutil:' .. snippets.username,  
message('display-sul')  
end  

function linkFunctions.tl(snippets)  
-- Target logs  
return makeFullUrlLink(  
snippets.interwiki,  
-1,  
'Log',  
{page = mw.site.namespaces[2].name .. ':' .. snippets.username},  
message('display-targetlogs')  
end  

function linkFunctions.efl(snippets)  
-- Edit filter log  
return makeFullUrlLink(  
snippets.interwiki,  
-1,  
'AbuseLog',  
{wpSearchUser = snippets.username},  
message('display-abuselog')  
end  

function linkFunctions.pr(snippets)  
-- Protections  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/protect/' .. snippets.username,  
message('display-protections')  
end  

function linkFunctions.rl(snippets)  
-- User rights  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/rights/' .. snippets.username,  
message('display-rights')  
end  

function linkFunctions.ren(snippets)  
-- Renames  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Log/renameuser/' .. snippets.username,  
message('display-renames')  
end  

function linkFunctions.rfa(snippets)  
-- Requests for adminship  
return makeWikilink(  
nil,  
-1,  
'PrefixIndex/' .. message('page-rfa') .. '/' .. snippets.username,  
message('display-rfa')  
end  

function linkFunctions.api(snippets)  
-- API user data  
return makeUrlLink(  
{  
host = snippets.fullDomain,  
path = '/w/api.php',  
query = {  
action = 'query',  
list = 'users',  
usprop = 'groups|editcount',  
ususers = snippets.username  
}  
},  
message('display-api')  
end  

function linkFunctions.up(snippets)  
-- Uploads  
return makeWikilink(  
snippets.interwiki,  
-1,  
'ListFiles/' .. snippets.username,  
message('display-uploads')  
end  

function linkFunctions.nuke(snippets)  
-- Mass delete/Special:Nuke  
return makeWikilink(  
snippets.interwiki,  
-1,  
'Nuke/' .. snippets.username,  
message('display-nuke')  

end  

---
-- End of link functions  
---

-- Define the metatable that memoizes the link functions, and fetches link  
-- functions from Module:UserLinks/extra if necessary.  

-- Lazily initialise the extraLinkFunctions table. We only want to load  
-- Module:UserLinks/extra as necessary, so it has a low transclusion  
-- count.  
local extraLinkFunctions  

-- Define functions for shared code in the metatable.  
local function validateCode(code)  
-- Checks whether code is a valid link code - i.e. checks that it is a  
-- string and that it is not the blank string. Returns the code if  
-- the check passes, and nil if not.  
if type(code) == 'string' and code ~= '' then  
return code  
else  
return nil  
end  
end  

local function getExtraLinkFunctions  
-- Loads the table of extra link functions from the /extra module.  
-- If there is a problem with loading it, return false. We use the  
-- distinction between false and nil to record whether we have already  
-- tried to load it.  
if extraLinkFunctions ~= nil then  
return extraLinkFunctions  
end  
if mExtra == nil then  
-- If loading the module fails, maybeLoadModule returns false.  
-- Here we use the distinction between false and nil to record  
-- whether we have already tried to load the /extra module.  
mExtra = maybeLoadModule('Module:UserLinks/extra')  
end  
if type(mExtra) == 'table'  
and type(mExtra.linkFunctions) == 'table'  
then  
extraLinkFunctions = mExtra.linkFunctions  
else  
extraLinkFunctions = false  
end  
return extraLinkFunctions  
end  

local function memoizeExtraLink(code, func)  
local success, link = pcall(func, snippets)  
if success and type(link) == 'string' then  
links[code] = link  
return link  
end  
return nil  
end  

-- Define the metatable.  
setmetatable(links, {  
__index = function (t, key)  
local code = validateCode(key)  
if not code then  
raiseError(  
message('error-malformedlinkcode'),  
message('error-malformedlinkcode-section')  
end  
local linkFunction = linkFunctions[code]  
local link  
if linkFunction then  
link = linkFunction(snippets)  
links[code] = link  
else  
extraLinkFunctions = getExtraLinkFunctions  
if extraLinkFunctions then  
local extraLinkFunction = extraLinkFunctions[code]  
if type(extraLinkFunction) == 'function' then  
link = memoizeExtraLink(code, extraLinkFunction)  
end  
end  
end  
if link then  
return link  
else  
raiseError(  
message('error-invalidlinkcode', code),  
message('error-invalidlinkcode-section')  
end  
end,  
__pairs = function  
extraLinkFunctions = getExtraLinkFunctions  
if extraLinkFunctions then  
for code, func in pairs(extraLinkFunctions) do  
if validateCode(code) and type(func) == 'function' then  
memoizeExtraLink(code, func)  
end  
end  
end  
-- Allow built-in functions to overwrite extra functions.  
for code, func in pairs(linkFunctions) do  
local link = func(snippets)  
links[code] = link  
end  
return function (t, key)  
return next(links, key)  
end  
end  
})  
return links  
end  

---
-- User data snippets  
---

function p.getSnippets(args)  
--[=[  
-- This function gets user data snippets from the arguments, and from  
-- Module:InterwikiTable. The data is loaded as necessary and memoized  
-- in the snippets table for performance.  
---
-- Snippets default to the blank string, '', so they can be used in  
-- concatenation operations without coders having to worry about raising  
-- errors. Because of this, the local functions snippetExists and  
-- getSnippet have been written to aid people writing new snippets. These  
-- functions treat the blank string as false. It is not necessary to return  
-- the blank string from a snippet function, as nil and false values are  
-- automatically converted into the blank string by the metatable.  
---
-- If you add a new snippet, please document it at  
-- Module:UserLinks#Adding new links.  
--]=]  
local snippets, snippetFunctions = {}, {}  
setmetatable(snippets, {  
__index = function (t, key)  
local snippetFunction = snippetFunctions[key]  
if snippetFunction then  
snippets[key] = snippetFunction or ''  
return snippets[key]  
else  
raiseError(  
message('error-nosnippet', key),  
message('error-nosnippet-section')  
end  
end  
})  

-- Define helper functions for writting the snippet functions.  
local function snippetExists(key)  
-- We have set the metatable up to make snippets default to '', so we  
-- don't have to test for false or nil.  
return snippets[key] ~= ''  
end  

local function getSnippet(key)  
local ret = snippets[key]  
if ret == '' then  
return nil  
else  
return ret  
end  
end  

-- Start snippet functions.  

function snippetFunctions.username  
-- The username.  
local username = args.user or args.User  
return username or raiseError(  
message('error-nousername'),  
message('error-nousername-section')  
end  

function snippetFunctions.usernameHtml  
-- The username html-encoded. Spaces are encoded as pluses.  
return mw.uri.encode(snippets.username)  
end  

function snippetFunctions.project  
-- The project name.  
-- Also does the work for snippetFunctions.interwikiTableKey, and adds  
-- the project value to snippets.lang if it is a valid language code.  
local project = args.Project or args.project  
if not project then  
return nil  
end  
local projectValidated, interwikiTableKey = p.validateProjectCode(project)  
if not projectValidated then  
if mw.language.isKnownLanguageTag(project) then  
if not snippetExists('lang') then  
snippets.lang = project  
end  
else  
raiseError(  
message('error-invalidproject', project),  
message('error-invalidproject-section')  
end  
end  
snippets.interwikiTableKey = interwikiTableKey  
return project  
end  

function snippetFunctions.interwikiTableKey  
-- The key for the project in Module:InterwikiTable.  
-- Relies on snippetFunctions.project to do the real work.  
local temp = snippets.project -- required; puts key in snippets table  
return rawget(snippets, 'interwikiTableKey')  
end  

function snippetFunctions.toolProject  
-- The short project code for use with toolserver or labs. It is always  
-- present, even if the "project" argument is absent. The default value  
-- is the "snippet-project-default" message.  
local project = getSnippet('project')  
if not project then  
return message('snippet-project-default')  
else  
return project  
end  
end  

function snippetFunctions.projectLong  
-- The long form of the project name, e.g. "wikipedia" or "wikibooks".  
local key = getSnippet('interwikiTableKey')  
if not key then  
return message('snippet-projectlong-default')  
end  
interwikiTable = interwikiTable or mw.loadData('Module:InterwikiTable')  
local prefixes = interwikiTable[key].iw_prefix  
-- Using prefixes[2] is a bit of a hack, but should find the long name  
-- most of the time.  
return prefixes[2] or prefixes[1]  
end  

function snippetFunctions.lang  
-- The language code.  
local lang = args.lang or args.Lang  
if not lang then  
return nil  
end  
if mw.language.isKnownLanguageTag(lang) then  
return lang  
else  
raiseError(  
message('error-invalidlanguage', lang),  
message('error-invalidlanguage-section')  
end  
end  

function snippetFunctions.toolLang  
-- The language code for use with toolserver or labs tools. It is always  
-- present, even if the "lang" argument is absent. The default value is  
-- the "snippet-lang-default" message.  
return getSnippet('lang') or message('snippet-lang-default')  
end  

function snippetFunctions.interwiki  
-- The interwiki prefix, consisting of the project and language values,  
-- separated by colons, e.g. ":wikt:es:".  
local project = getSnippet('project')  
local lang = getSnippet('lang')  
if not project and not lang then  
return nil  
end  
local ret = {}  
ret[#ret + 1] = project  
ret[#ret + 1] = lang  
return table.concat(ret, ':')  
end  

function snippetFunctions.fullDomain  
-- The full domain name of the site, e.g. www.mediawiki.org,  
-- en.wikpedia.org, or ja.wikibooks.org.  
local fullDomain  
local lang = getSnippet('toolLang')  
local key = getSnippet('interwikiTableKey')  
if key then  
interwikiTable = interwikiTable or mw.loadData('Module:InterwikiTable')  
local domain = interwikiTable[key].domain  
local takesLangPrefix = interwikiTable[key].takes_lang_prefix  
if takesLangPrefix then  
fullDomain = lang .. '.' .. domain  
else  
fullDomain = domain  
end  
else  
fullDomain = lang .. '.wikipedia.org'  
end  
return fullDomain  
end  

-- End snippet functions. If you add a new snippet function, please  
-- document it at Module:UserLinks#Adding new links.  

return snippets  
end  

function p.validateProjectCode(s)  
-- Validates a project code, by seeing whether it is present in  
-- Module:InterwikiTable. If it is present, returns the code and the  
-- InterwikiTable key for the corresponding site. If not present,  
-- returns nil for both.  
interwikiTable = interwikiTable or mw.loadData('Module:InterwikiTable')  
for key, t in pairs(interwikiTable) do  
for i, prefix in ipairs(t.iw_prefix) do  
if s == prefix then  
return s, key  
end  
end  
end  
return nil, nil  
end  

---
-- Main functions  
---

local function makeInvokeFunction(funcName)  
-- Makes a function that can be accessed from #invoke. This is only required  
-- for functions that need to access arguments.  
return function  
mArguments = require('Module:Arguments')  
local args = mArguments.getArgs  
return p[funcName](args)  
end  
end  

p.main = makeInvokeFunction('_main')  

function p._main(args)  
-- The main function. This is the one called from Template:User-multi,  
-- via p.main.  
local options = p.getOptions(args)  
local snippets = p.getSnippets(args)  
local codes = p.getCodes(args)  
local links = p.getLinks(snippets)  
-- Overload the built-in Lua error function to generate wikitext errors  
-- meant for end users to see. This makes things harder to debug when  
-- real errors occur, but it is the only realistic way to show wikitext  
-- errors and and still have sane code when using metatables, etc.  
local success, result = pcall(p.export, codes, links, options)  
if success then  
return result  
else  
return makeWikitextError(result, options.isDemo)  
end  
end  

function p.getOptions(args)  
-- Gets the options from the args table, so that we don't have to pass  
-- around the whole args table all the time.  
local options = {}  
options.isDemo = yesno(args.demo) or false  
options.noPing = yesno(args.noPing) or yesno(args.noping) or yesno(args.np) or false  
options.toolbarStyle = yesno(args.small) and 'font-size: 90%;' or nil  
options.sup = yesno(args.sup, true)  
options.separator = args.separator  
options.span = args.span  
return options  
end  

function p.getCodes(args)  
-- Gets the link codes from the arguments. The codes aren't validated  
-- at this point.  
mTableTools = maybeLoadModule('Module:TableTools')  
local codes  
if mTableTools then  
codes = mTableTools.compressSparseArray(args)  
else  
codes = {}  
for i, code in ipairs(args) do  
codes[i] = code  
end  
end  
return codes  
end  

function p.export(codes, links, options)  
-- Make the user link.  
local userLink = options.noPing and links.np or links.u  

-- If we weren't passed any link codes, just return the user link.  
if #codes < 1 then  
return userLink  
end  

-- Make the toolbar.  
mToolbar = require('Module:Toolbar')  
local toolbarArgs = {}  
for i, code in ipairs(codes) do  
local link = links[code]  
toolbarArgs[#toolbarArgs + 1] = link  
end  
toolbarArgs.style = options.toolbarStyle  
toolbarArgs.separator = options.separator or 'dot'  
toolbarArgs.span = options.span  
local toolbar = mToolbar.main(toolbarArgs)  

-- Apply the sup option.  
if options.sup then  
toolbar = '<sup>' .. toolbar .. '</sup>'  
end  

-- If we are transcluding, add a non-breaking space, but if we are substing  
-- just use a normal space  
local space = mw.isSubsting and ' ' or '&nbsp;'  

return userLink .. space .. toolbar  
end  

---
-- Single link function  
---

p.single = makeInvokeFunction('_single')  

function p._single(args)  
-- Fetches a single link from the link table.  
local options = p.getOptions(args)  
local snippets = p.getSnippets(args)  
local links = p.getLinks(snippets)  
local code = args[1]  
local success, link = pcall(p.exportSingle, links, code)  
if success then  
return link  
else  
return makeWikitextError(link, options.isDemo)  
end  
end  

function p.exportSingle(links, code)  
-- If any errors occur, they will probably occur here. This function  
-- exists purely so that all the errors that will occur in p._single can  
-- be handled using a single pcall.  
if not code then  
raiseError(  
message('error-nolinkcode'),  
message('error-nolinkcode-section')  
end  
return links[code]  
end  

---
-- Link table  
---

function p.linktable  
-- Returns a wikitext table of link codes, with an example link for each  
-- one. This function doesn't take any arguments, so it can be accessed  
-- directly from wiki pages without using makeInvokeFunction.  
local args = {user = 'Example'}  
local snippets = p.getSnippets(args)  
local links = p.getLinks(snippets)  

-- Assemble the codes and links in order  
local firstCodes = {'u', 't', 'c'}  
local firstLinks, firstCodesKeys = {}, {}  
for i, code in ipairs(firstCodes) do  
firstCodesKeys[code] = true  
firstLinks[#firstLinks + 1] = {code, links[code]}  
end  
local secondLinks = {}  
for code, link in pairs(links) do  
if not firstCodesKeys[code] then  
secondLinks[#secondLinks + 1] = {code, link}  
end  
end  
table.sort(secondLinks, function(t1, t2)  
return t1[1] < t2[1]  
end)  
local links = {}  
for i, t in ipairs(firstLinks) do  
links[#links + 1] = t  
end  
for i, t in ipairs(secondLinks) do  
links[#links + 1] = t  
end  

-- Output the code table in table format  
local ret = {}  
ret[#ret + 1] = '{|'  
ret[#ret + 1] = '|-'  
ret[#ret + 1] = '! ' .. message('linktable-codeheader')  
ret[#ret + 1] = '! ' .. message('linktable-previewheader')  
for i, t in ipairs(links) do  
local code = t[1]  
local link = t[2]  
ret[#ret + 1] = '|-'  
ret[#ret + 1] = "| **" .. code .. "**"  
ret[#ret + 1] = '| ' .. link  
end  
ret[#ret + 1] = '|}'  
return table.concat(ret, '\n')  
end  

return p