Module:DynamicYSWSList

From Hack Club Wiki
Revision as of 14:17, 5 November 2026 by U080F22CTJN (talk | contribs) (add auto link formatting?)
Jump to navigation Jump to search

Documentation for this module may be created at Module:DynamicYSWSList/doc

-- Module:DynamicYSWSList
-- Public API only. Wikitext table avoids unsupported HTML tags in MediaWiki.
local p = {}
local API_URL = 'https://hackclub.com/api/v1/events'
local MONTHS = {'January','February','March','April','May','June','July','August','September','October','November','December'}

local function str(v)
    if type(v) ~= 'string' and type(v) ~= 'number' then return '' end
    return mw.text.trim(tostring(v))
end
local function first(...)
    for i = 1, select('#', ...) do
        local v = str(select(i, ...))
        if v ~= '' then return v end
    end
    return ''
end
local function safe(v)
    -- Escape user/API text so it cannot inject table delimiters or wiki markup.
    return mw.text.nowiki(str(v))
end
local function isoDate(v)
    local y,m,d = str(v):match('^(%d%d%d%d)%-(%d%d)%-(%d%d)')
    if not y then return nil end
    y,m,d = tonumber(y),tonumber(m),tonumber(d)
    if m < 1 or m > 12 then return nil end
    local lengths = {31,28,31,30,31,30,31,31,30,31,30,31}
    if y % 4 == 0 and (y % 100 ~= 0 or y % 400 == 0) then lengths[2] = 29 end
    if d < 1 or d > lengths[m] then return nil end
    return string.format('%04d-%02d-%02d',y,m,d)
end
local function displayDate(date)
    if not date then return '—' end
    local y,m,d = date:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)$')
    return string.format('%d %s %s', tonumber(d), MONTHS[tonumber(m)], y)
end
local function dateCell(date, class)
    return '| data-sort-value="' .. (date or '9999-12-31') .. '" class="' .. class .. '" | ' .. displayDate(date)
end
local function validUrl(v)
    local url = str(v)
    if url:match('^https?://[^%s%[%]<>|{}"\']+$') then return url end
    return nil
end
local function description(v)
    local s = str(v):gsub('<[^>]*>', ''):gsub('%s+', ' ')
    s = mw.text.trim(mw.text.decode(s))
    if mw.ustring.len(s) > 240 then s = mw.ustring.sub(s, 1, 237) .. '…' end
    return s
end
local function fetch()
    if not (mw.ext and mw.ext.externalData and type(mw.ext.externalData.getExternalData) == 'function') then
        return nil, 'External Data is unavailable.'
    end
    local ok, rows = pcall(mw.ext.externalData.getExternalData, {
        url = API_URL,
        format = 'json with jsonpath',
        data = {
            name = '$.data[*].name',
            description = '$.data[*].description',
            url = '$.data[*].url',
            startDate = '$.data[*].startDate',
            endDate = '$.data[*].endDate'
        }
    })
    if not ok or type(rows) ~= 'table' then return nil, 'Could not load public events.' end
    return rows
end
function p.main(frame)
    local data, err = fetch()
    if not data then return '<strong class="error">' .. safe(err) .. '</strong>' end
    local programs = {}
    for _, row in ipairs(data) do
        if type(row) == 'table' then
            local name = first(row.name, row.title)
            if name ~= '' then
                programs[#programs + 1] = {
                    name = name,
                    description = description(first(row.description, row.summary)),
                    startDate = isoDate(first(row.startDate,row.start_date,row.start)),
                    endDate = isoDate(first(row.endDate,row.end_date,row['end'])),
                    url = validUrl(first(row.url,row.website,row.link))
                }
            end
        end
    end
    table.sort(programs, function(a,b)
        return mw.ustring.lower(a.name) < mw.ustring.lower(b.name)
    end)
    if #programs == 0 then return 'No public programs were returned.' end
    local out = {
        '<div class="ysws-directory">',
        '<p class="ysws-directory-count">' .. #programs .. ' public event entries listed.</p>',
        '<div class="ysws-table-wrapper">',
        '{| class="wikitable sortable ysws-table"',
        '! Program\n! Description\n! Start date\n! End date\n! Status\n! Website'
    }
    for _, item in ipairs(programs) do
        out[#out+1] = '|- class="ysws-program" data-start="' .. (item.startDate or '') .. '" data-end="' .. (item.endDate or '') .. '"'
        out[#out+1] = string.format('! scope="row" class="ysws-name" | [[%s|%s]]' ..  item.name, safe(item.name))
        out[#out+1] = '| class="ysws-description" | ' .. (item.description ~= '' and safe(item.description) or '—')
        out[#out+1] = dateCell(item.startDate, 'ysws-start')
        out[#out+1] = dateCell(item.endDate, 'ysws-end')
        out[#out+1] = '| class="ysws-status" | Unknown'
        out[#out+1] = '| class="ysws-website" | ' .. (item.url and ('[' .. item.url .. ' Website]') or '—')
    end
    out[#out+1] = '|}'
    out[#out+1] = '</div>'
    out[#out+1] = '<p class="ysws-source-note">Generated from the public Hack Club events API. Dates and descriptions reflect the public feed.</p>'
    out[#out+1] = '</div>'
    return frame:preprocess(table.concat(out, '\n'))
end
return p