Nathan Schneider 2028f1ee85 Refactored policy structure
Previously, all modules in an org were fully copied into that
org. Now, the only copy of the modules is at modpol.modules, and orgs
have a policy table at [org].policies, which overrides the config
table in any given module.
2022-02-09 22:14:26 -07:00

612 lines
21 KiB
Lua

--- INTERACTIONS.LUA (for Minetest)
-- CONTEXTUAL STUFF
-- ================
-- _contexts to enable passing across formspecs
-- https://rubenwardy.com/minetest_modding_book/en/players/formspecs.html#contexts
local _contexts = {}
local function get_context(name)
local context = _contexts[name] or {}
_contexts[name] = context
return context
end
minetest.register_on_leaveplayer(function(player)
_contexts[player:get_player_name()] = nil
end)
-- UTILITIES
-- =========
-- Function: formspec_list
-- for use generating option lists in formspecs from tables
-- input: table of strings
-- output: a formspec-ready list of the strings
local function formspec_list(array)
local escaped = {}
if not array then
return ""
end
for i = 1, #array do
escaped[i] = minetest.formspec_escape(array[i])
end
return table.concat(escaped,",")
end
-- DASHBOARDS
-- ==========
-- Function: modpol.interactions.dashboard(user)
-- Params: user (string)
-- Q: Should this return a menu of commands relevant to the specific user?
-- Output: Displays a menu of commands to the user
-- TODO currently a manually curated list---needs major improvement
function modpol.interactions.dashboard(user)
-- prepare data
-- to add: nested orgs map
local all_orgs = modpol.orgs.list_all()
local user_orgs = modpol.orgs.user_orgs(user)
local all_users = modpol.instance:list_members()
-- pending list
local user_pending_count = 0
local user_pending = {}
for k,v in ipairs(modpol.orgs.array) do
if v.pending and v.pending[user] then
if modpol.util.num_pairs(v.pending[user]) ~= 0 then
table.insert(user_pending, v.name)
user_pending_count = user_pending_count + 1
end
end
end
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,8]",
"hypertext[0.5,0.5;9,1;title;<big>Org dashboard</big>]",
"label[0.5,2;All orgs:]",
"dropdown[2.5,1.5;7,0.8;all_orgs;View...,"..formspec_list(all_orgs)..";;]",
"label[0.5,3;Your orgs:]",
"dropdown[2.5,2.5;7,0.8;user_orgs;View...,"..formspec_list(user_orgs)..";;]",
"label[0.5,4;All users:]",
"dropdown[2.5,3.5;7,0.8;all_users;View...,"..formspec_list(all_users)..";;]",
"label[0.5,5;Pending ("..user_pending_count.."):]",
"dropdown[2.5,4.5;7,0.8;pending;View...,"..formspec_list(user_pending)..";;]",
"button[0.5,7;1,0.8;refresh;Refresh]",
"button_exit[8.5,7;1,0.8;close;Close]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(user, "modpol:dashboard", formspec_string)
end
-- receive input
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:dashboard" then
local pname = player:get_player_name()
if nil then
elseif fields.close then
minetest.close_formspec(pname, formname)
elseif fields.refresh then
modpol.interactions.dashboard(pname)
-- Put all dropdowns at the end
elseif fields.all_users
and fields.all_users ~= "View..." then
modpol.interactions.user_dashboard(
pname,
fields.all_users,
function()
modpol.interactions.dashboard(pname)
end
)
elseif fields.all_orgs or fields.user_orgs or fields.pending then
local org_name = fields.all_orgs or fields.user_orgs or fields.pending
if org_name ~= "View..." then
modpol.interactions.org_dashboard(
pname, org_name)
end
end
end
end)
-- Function: modpol.interactions.org_dashboard
-- Params: user (string), org_string (string or num)
-- Output: Displays a menu of org-specific commands to the user
function modpol.interactions.org_dashboard(user, org_string)
-- prepare data
local org = modpol.orgs.get_org(org_string)
if not org then return nil end
local function membership_toggle(org_display)
local current_org = modpol.orgs.get_org(org_display)
if current_org then
if current_org:has_member(user) then
return " (member)"
end
end
return ""
end
-- identify parent
local parent = modpol.orgs.get_org(org.parent)
if parent then parent = parent.name
else parent = "none" end
-- prepare members menu
local members = org.members
-- prepare children menu
local children = {}
for k,v in ipairs(org.children) do
local this_child = modpol.orgs.get_org(v)
table.insert(children, this_child.name)
end
table.sort(children)
-- prepare modules menu
local modules = {}
if modpol.modules then
for k,v in pairs(modpol.modules) do
if not v.hide and -- hide utility modules
org.policies[k] then -- org includes it
local module_entry = v.name
table.insert(modules, module_entry)
end
end
end
table.sort(modules)
-- prepare pending menu
local pending = {}
local num_pending = 0
if org.pending[user] then
for k,v in pairs(org.pending[user]) do
if org.processes[k] ~= "deleted" then
local pending_string = org.processes[k].name
.." ["..k.."]"
table.insert(pending, pending_string)
num_pending = num_pending + 1
end
end
end
table.sort(pending)
-- set player context
local user_context = {}
user_context["current_org"] = org.name
_contexts[user] = user_context
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,8]",
"hypertext[0.5,0.5;9,1;title;<big>Org: <b>"..
minetest.formspec_escape(org.name).."</b>"..membership_toggle(org.name).."</big>]",
"label[0.5,1.25;Parent: "..parent..membership_toggle(parent).."]",
"label[0.5,2;Members:]",
"dropdown[2.5,1.5;7,0.8;members;View...,"..formspec_list(members)..";;]",
"label[0.5,3;Child orgs:]",
"dropdown[2.5,2.5;7,0.8;children;View...,"..formspec_list(children)..";;]",
"label[0.5,4;Modules:]",
"dropdown[2.5,3.5;7,0.8;modules;View...,"..formspec_list(modules)..";;]",
"label[0.5,5;Pending ("..num_pending.."):]",
"dropdown[2.5,4.5;7,0.8;pending;View...,"..formspec_list(pending)..";;]",
"button[0.5,7;1,0.8;refresh;Refresh]",
"button[8.5,7;1,0.8;back;Back]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(user, "modpol:org_dashboard", formspec_string)
end
-- receive input
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:org_dashboard" then
local pname = player:get_player_name()
local org = modpol.orgs.get_org(_contexts[pname].current_org)
-- just confirm the org still exists:
if not org then
modpol.interactions.message(pname, "Org no longer exists")
modpol.interactions.dashboard(pname)
return end
-- okay, onward
if nil then
elseif fields.back then
modpol.interactions.dashboard(pname)
elseif fields.refresh then
modpol.interactions.org_dashboard(pname, org.name)
-- Put all dropdowns at the end
-- Receiving modules
elseif fields.members
and fields.members ~= "View..." then
modpol.interactions.user_dashboard(
pname,
fields.members,
function()
modpol.interactions.org_dashboard(
pname, org.name)
end
)
elseif fields.modules
and fields.modules ~= "View..." then
local module = nil
for k,v in pairs(modpol.modules) do
if fields.modules == v.name
and org.policies[v.slug] then
module = v
end end
if module then
modpol.interactions.binary_poll_user(
pname,
module.name..":\n"..
module.desc.."\n"..
modpol.interactions.get_policy_string(
org.name, module.slug, ", ")..
"\n".."Proceed?",
function(input)
if input == "Yes" then
org:call_module(module.slug, pname)
elseif input == "No" then
modpol.interactions.org_dashboard(
pname, org.id)
end
end)
end
-- Receiving pending
elseif fields.pending
and fields.pending ~= "View..." then
local pending = string.match(
fields.pending,"%[(%d)%]")
local process = org.processes[tonumber(pending)]
if process then
org:interact(process.id, pname)
end
-- Children
elseif fields.children
and fields.children ~= "View..." then
local org_name = fields.children
modpol.interactions.org_dashboard(pname, org_name)
end
end
end)
--- Function: modpol.interactions.user_dashboard
-- Displays a dashboard about a particular user
-- @param viewer Name of user viewing the dashboard (string)
-- @param user Name of user being viewed (string)
-- @param completion Optional function to call on Done button
function modpol.interactions.user_dashboard(viewer, user, completion)
local user_orgs = modpol.orgs.user_orgs(user)
-- set player context
local user_context = {}
user_context["viewer"] = viewer
user_context["user"] = user
user_context["completion"] = completion
_contexts[viewer] = user_context
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,8]",
"hypertext[0.5,0.5;9,1;title;<big>User: <b>"..user.."</b></big>]",
"label[0.5,2;User's orgs:]",
"dropdown[2.5,1.5;7,0.8;user_orgs;View...,"..formspec_list(user_orgs)..";;]",
"button[0.5,7;1.5,0.8;message;Message]",
"button_exit[8.5,7;1,0.8;close;Close]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(viewer, "modpol:user_dashboard", formspec_string)
end
-- receive input
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:user_dashboard" then
local contexts = _contexts[player:get_player_name()]
-- check fields
if nil then
elseif fields.message then
modpol.interactions.message_user(
contexts.viewer, contexts.user
)
elseif fields.back then
if contexts.completion then
completion()
else
modpol.interactions.dashboard(
contexts.viewer)
end
-- dropdown fields
elseif fields.user_orgs
and fields.user_orgs ~= "View..." then
modpol.interactions.org_dashboard(
contexts.viewer, fields.user_orgs)
end
end
end)
-- INTERACTION PRIMITIVES
-- ======================
-- Function: modpol.interactions.message
-- Produces a brief message to a user
-- input: user (string), message (string)
-- output: displays message to specified user
function modpol.interactions.message(user, message)
if message then
minetest.chat_send_player(user, message)
end
end
--- Function: modpol.interactions.message_user
-- Gets and sends a message from one user to another
-- @param sender Name of user sending (string)
-- @param recipient Name of user receiving (string)
function modpol.interactions.message_user(sender, recipient)
modpol.interactions.text_query(
sender,
"Message for "..recipient..":",
function(input)
modpol.interactions.message(
recipient,
input.." [from "..sender.."]")
end
)
end
--- Function: modpol.interactions.display
-- Displays complex data to a user
-- @param user Name of target user (string)
-- @param title Title of display (string)
-- @param message Content of message (string or table of strings)
-- @param completion Optional function for what happens when user is done
function modpol.interactions.display(
user, title, message, completion)
-- set up contexts
_contexts[user]["completion"] = completion
-- set up output
local output = ""
if type(message) == "table" then
output = table.concat(message,"\n")
elseif type(message) == "string" then
output = message
elseif type(message) == "number" then
output = message
else
modpol.interactions.message(
self.initiator, "Error: message not typed for display")
if completion then completion() else
modpol.intereactions.dashboard(user)
end
return
end
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,8]",
"label[0.5,0.5;"..title.."]",
"hypertext[0.5,1;9,5.5;display;<global background=black margin=10>"..output.."]",
"button_exit[8.5,7;1,0.8;done;Done]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(user, "modpol:display", formspec_string)
end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
local pname = player:get_player_name()
if formname == "modpol:display" then
if fields.done and _contexts[pname].completion then
minetest.close_formspec(pname, formname)
_contexts[pname].completion()
end
end
end)
-- Function: modpol.interactions.text_query
-- Overrides function at modpol/interactions.lua
-- input: user (string), query (string), func (function)
-- output: Applies "func" to user input
function modpol.interactions.text_query(user, query, func)
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,6]",
"label[0.5,1;", minetest.formspec_escape(query), "]",
"field[0.5,3.25;9,0.8;input;;]",
"button[0.5,4.5;1,0.8;yes;OK]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(user, "modpol:text_query", formspec_string)
-- put func in _contexts
if _contexts[user] == nil then _contexts[user] = {} end
_contexts[user]["text_query_func"] = func
end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:text_query" then
local pname = player:get_player_name()
local input = fields.input
if not input then
-- no input, do nothing
else
local func = _contexts[pname]["text_query_func"]
if func then
func(input)
else
modpol.interactions.message(pname, "text_query: " .. input)
end
end
minetest.close_formspec(pname, formname)
end
end)
-- Function: dropdown_query
-- input: user (string), label (string), options (table of strings), func (function)
-- func input: choice (string)
-- output: calls func on user choice
function modpol.interactions.dropdown_query(user, label, options, func)
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,4]",
"label[0.5,1;"..minetest.formspec_escape(label).."]",
"dropdown[0.5,1.25;9,0.8;input;View...,"..formspec_list(options)..";;]",
"button[0.5,2.5;1,0.8;cancel;Cancel]",
}
local formspec_string = table.concat(formspec, "")
-- present to players
minetest.show_formspec(user, "modpol:dropdown_query", formspec_string)
-- put func in _contexts
if _contexts[user] == nil then _contexts[user] = {} end
_contexts[user]["dropdown_query_func"] = func
end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:dropdown_query" then
local pname = player:get_player_name()
if fields.cancel then
minetest.close_formspec(pname, formname)
elseif fields.input == "View..." then
-- "View...", do nothing
else
local choice = fields.input
local func = _contexts[pname]["dropdown_query_func"]
if not choice then
-- empty, do nothing
elseif func then
--causes issues with sequential dropdowns
--minetest.close_formspec(pname, formname)
func(choice)
else
minetest.close_formspec(pname, formname)
modpol.interactions.message(pname, "dropdown_query: " .. choice)
end
end
end
end)
--- Function: modpol.interactions.checkbox_query
-- Allows user to select from a set of options
-- @param user Name of user (string)
-- @param label Query for user before options (string)
-- @param options table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
-- @param func function to be called with param "input", made up of the corrected table in the same format as the param options
function modpol.interactions.checkbox_query(
user, label, options, func)
-- set up formspec
-- prepare options
local vertical = 0
local checkbox_options = {}
for i,v in ipairs(options) do
local fs_line = ""
vertical = i * .5
fs_line = "checkbox[0,"..vertical..";checkbox_"..i..";"..
minetest.formspec_escape(v[1])..";"..
tostring(v[2]).."]"
table.insert(checkbox_options, fs_line)
end
local max = vertical * 4
local bar_height = vertical / 2
local formspec = {
"formspec_version[4]",
"size[10,8]",
"label[0.5,0.5;"..label.."]",
"scrollbaroptions[arrows=default;max="..max..";smallstep=10;largestep=100;thumbsize="..bar_height.."]",
"scrollbar[9,1;0.3,5.5;vertical;scroller;0]",
"scroll_container[0.5,1;9,5.5;scroller;vertical]",
}
-- insert options
for i,v in ipairs(checkbox_options) do
table.insert(formspec, v)
end
table.insert(formspec,"scroll_container_end[]")
table.insert(formspec,"button[0.5,7;1.5,0.8;submit;Submit]")
table.insert(
formspec,"button_exit[8,7;1.5,0.8;cancel;Cancel]")
local formspec_string = table.concat(formspec, "")
-- present to players
minetest.show_formspec(user, "modpol:checkbox_query", formspec_string)
-- put func in _contexts
if _contexts[user] == nil then _contexts[user] = {} end
_contexts[user]["checkbox_query_func"] = func
_contexts[user]["checkbox_query_result"] =
modpol.util.copy_table(options)
end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:checkbox_query" then
local pname = player:get_player_name()
-- start checking fields
if fields.cancel then
minetest.close_formspec(pname, formname)
elseif fields.submit then
-- send in result
minetest.close_formspec(pname, formname)
_contexts[pname].checkbox_query_func(
_contexts[pname].checkbox_query_result)
else
for k,v in pairs(fields) do
-- identify checkbox actions and flip bool
if string.find(k,"checkbox_") then
local index = tonumber(
string.match(k,"%d+"))
_contexts[pname].checkbox_query_result[index][2] =
not _contexts[pname].checkbox_query_result[index][2]
end
end
end
end
end)
-- Function: modpol.binary_poll_user(user, question, function)
-- Overrides function at modpol/interactions.lua
-- Params: user (string), question (string), func (function)
-- func input: user input (string: y/n)
-- Output: Applies "func" to user input
function modpol.interactions.binary_poll_user(user, question, func)
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[8,6]",
"label[0.375,0.5;",minetest.formspec_escape(question), "]",
"button[1,5;1,0.8;yes;Yes]",
"button[2,5;1,0.8;no;No]",
--TODO can we enable text wrapping?
--TODO we could use scroll boxes to contain the text
}
local formspec_string = table.concat(formspec, "")
if _contexts[user] == nil then _contexts[user] = {} end
_contexts[user]["binary_poll_func"] = func
-- present to player
minetest.show_formspec(user, "modpol:binary_poll_user", formspec_string)
end
minetest.register_on_player_receive_fields(function (player, formname, fields)
local pname = player:get_player_name()
-- modpol:binary_poll
if formname == "modpol:binary_poll_user" then
local vote = nil
if fields.yes then vote = fields.yes
elseif fields.no then vote = fields.no
end
if vote then
local func = _contexts[pname]["binary_poll_func"]
if func then func(vote) end
end
minetest.close_formspec(pname, formname)
end
end)
-- TESTING
--testing command for "singleplayer"
function modpol.msg(text)
modpol.interactions.message("singleplayer",text)
end