diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..5283261 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,16 @@ +One administrator, @ntnsndr, holds ultimate decision-making power over the project. This is a temporary arrangement while we build trust as a community and adopt a more inclusive structure. + +* **Autocracy** The administrator holds ultimate decision-making power in the community. + * **Executive** The administrator is responsible for implementing—or delegating implementation of—policies and other decisions. + * **Lobbying** If participants are not happy with the administrator's leadership, they may voice their concerns or leave the community. +* **Do-ocracy** Those who step forward to do a given task can decide how it should be done, in ongoing consultation with each other and the administrator. + * **Membership** Participation is open to anyone who wants to contribute. The administrator can remove misbehaving participants at will for the sake of the common good. +* **Code of Conduct** Participants are expected to abide by the Contributor Covenant (contributor-covenant.org). +* **Ownership** This is a project of the Media Enterprise Design Lab at the University of Colorado Boulder and is owned by the university. + +--- + +Created by [Nathan Schneider](https://gitlab.com/medlabboulder/modpol) + +[![CommunityRule derived](https://communityrule.info/assets/CommunityRule-derived-000000.svg)](https://communityrule.info) +[Creative Commons BY-SA](https://creativecommons.org/licenses/by-sa/4.0/) diff --git a/modpol_minetest/chatcommands.lua b/modpol_minetest/chatcommands.lua new file mode 100644 index 0000000..33c0ca6 --- /dev/null +++ b/modpol_minetest/chatcommands.lua @@ -0,0 +1,108 @@ +-- =================================================================== +-- Minetest commands +-- =================================================================== + +command_list = {} -- user-facing table of commands + +local chat_table -- MT chat command definitions table +local regchat -- Chat-command registration function + +regchat = minetest.register_chatcommand + +regchat = function(name, command_table) + minetest.register_chatcommand(name, command_table) + table.insert(command_list, name) +end + +-- =================================================================== +-- /modpol +-- Presents a menu of options to users +regchat( + "modpol", { + privs = {}, + func = function(user) + modpol.interactions.dashboard(user) + end, +}) + +-- =================================================================== +-- /reset +-- For testing only +-- Clears the system and recreates instance with all players +regchat( + "reset", { + privs = {}, + func = function(user) + modpol.orgs.reset(); + return true, "Reset orgs" + end, +}) + + + +-- =================================================================== +-- /addorg +-- This code defines a chat command which creates a new +-- "org". Presently, the command makes the user the sole member of the +-- "org". + +regchat( + "addorg", { + privs = {} , + func = function (user, param) + local success, message = modpol.instance:add_org (param) + return true, message + end +}) + +-- =================================================================== +-- /listorgs +-- In Minetest mode, this code defines a chat command which lists +-- existing "orgs". +-- The list shows one "org" per line in the following format: +-- org_name (member, member, ...) + +regchat( + "listorgs", { + privs = {} , + func = function (user, param) + return true, "Orgs: " .. + table.concat(modpol.orgs.list_all(), ", ") + end +}) + +-- =================================================================== +-- /listplayers +regchat( + "listplayers", { + privs = {}, + func = function(user) + local result = table.concat(modpol.list_users(),", ") + return true, "All players: " .. result + end, +}) + +-- =================================================================== +-- /joinorg +regchat( + "joinorg", { + privs = {}, + func = function(user, param) + local org = modpol.orgs.get_org(param) + local success, result = org:add_member(user) + return true, result + end, +}) + + +-- =================================================================== +-- /pollself [question] +-- asks the user a question specified in param +regchat( + "pollself", { + privs = {}, + func = function(user, param) + modpol.interactions.binary_poll_user(user, param) + return true, result + end, +}) diff --git a/modpol_minetest/overrides/interactions.lua b/modpol_minetest/overrides/interactions.lua new file mode 100644 index 0000000..b109a36 --- /dev/null +++ b/modpol_minetest/overrides/interactions.lua @@ -0,0 +1,479 @@ +-- 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.list_users() + -- set up formspec + local formspec = { + "formspec_version[4]", + "size[10,8]", + "label[0.5,0.5;MODPOL DASHBOARD]", + "label[0.5,2;All orgs:]", + "dropdown[2,1.5;5,0.8;all_orgs;"..formspec_list(all_orgs)..";;]", + "label[0.5,3;Your orgs:]", + "dropdown[2,2.5;5,0.8;user_orgs;"..formspec_list(user_orgs)..";;]", + "label[0.5,4;All users:]", + "dropdown[2,3.5;5,0.8;all_users;"..formspec_list(all_users)..";;]", + "button[0.5,7;1,0.8;test_poll;Test poll]", + "button[2,7;1,0.8;add_org;Add org]", + "button[3.5,7;1.5,0.8;remove_org;Remove org]", + "button[5.5,7;1.5,0.8;reset_orgs;Reset orgs]", + "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 + -- buttons first + elseif fields.test_poll then + -- FOR TESTING PURPOSES ONLY + modpol.interactions.text_query( + pname,"Poll question:", + function(input) + modpol.interactions.binary_poll_user( + pname, input, + function(vote) + modpol.interactions.message( + pname, pname .. " voted " .. vote) + end) + end) + elseif fields.add_org then + modpol.interactions.add_org(pname, 1) + elseif fields.remove_org then + modpol.interactions.remove_org(pname) + elseif fields.reset_orgs then + modpol.orgs.reset() + modpol.instance:add_member(pname) + modpol.interactions.dashboard(pname) + + -- Put all dropdowns at the end + elseif fields.close then + minetest.close_formspec(pname, formname) + elseif fields.all_orgs or fields.user_orgs then + local org_name = fields.all_orgs or fields.user_orgs + modpol.interactions.org_dashboard(pname, org_name) + end + end +end) + + +-- Function: modpol.interactions.org_dashboard +-- Params: user (string), org_name (string) +-- Output: Displays a menu of org-specific commands to the user +function modpol.interactions.org_dashboard(user, org_name) + -- prepare data + local org = modpol.orgs.get_org(org_name) + if not org then return nil end + local is_member = org:has_member(user) + local membership_toggle = function() + local toggle_code = "" + if is_member then + toggle_code = toggle_code + ..minetest.formspec_escape("leave")..";" + ..minetest.formspec_escape("Leave").."]" + else + toggle_code = toggle_code + ..minetest.formspec_escape("join")..";" + ..minetest.formspec_escape("Join").."]" + end + return toggle_code + end + + -- identify parent + local parent = modpol.orgs.get_org(org.parent) + if parent then parent = parent.name + else parent = "none" end + + -- prepare children menu + local children = {"View..."} + for k,v in ipairs(org.children) do + local this_child = modpol.orgs.get_org(v) + table.insert(children, this_child.name) + end + + -- prepare modules menu + local modules = {"View..."} + if org.modules then + for k,v in ipairs(org.modules) do + table.insert(modules, org.modules[k].slug) + end + end + + -- prepare actions menu + local actions = {"View..."} + if org.pending[user] then + for k,v in pairs(org.pending[user]) do + local action_string = "[" .. k .. "] " .. + org.processes[k].name + table.insert(actions, action_string) + end + end + + -- 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]", + "label[0.5,0.5;Org: ".. + minetest.formspec_escape(org_name).."]", + "label[0.5,1;Parent: "..parent.."]", + "button[8.5,0.5;1,0.8;"..membership_toggle(), + "label[0.5,2;Members:]", + "dropdown[2,1.5;5,0.8;user_orgs;"..formspec_list(org.members)..";;]", + "label[0.5,3;Children:]", + "dropdown[2,2.5;5,0.8;children;"..formspec_list(children)..";;]", + "label[0.5,4;Modules:]", + "dropdown[2,3.5;5,0.8;modules;"..formspec_list(modules)..";;]", + "label[0.5,5;Actions:]", + "dropdown[2,4.5;5,0.8;actions;"..formspec_list(actions)..";;]", + "button[0.5,7;1,0.8;test_poll;Test poll]", + "button[2,7;1,0.8;add_child;Add child]", + "button[3.5,7;1.5,0.8;remove_org;Remove org]", + "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) + if nil then + elseif fields.join then + local new_request = { + user = pname, + type = "add_member", + params = {pname} + } + org:make_request(new_request) + modpol.interactions.org_dashboard(pname,org.name) + elseif fields.leave then + org:remove_member(pname) + modpol.interactions.dashboard(pname) + elseif fields.test_poll then + modpol.interactions.binary_poll_org( + pname, org.id, + function(input) + modpol.interactions.message_org( + pname, + org.id, + "New response: " .. input) + end) + elseif fields.add_child then + modpol.interactions.text_query( + pname, "Child org name:", + function(input) + local new_request = { + user = pname, + type = "add_org", + params = {input} + } + org:make_request(new_request) + modpol.interactions.message(pname,"requested") + modpol.interactions.org_dashboard( + pname,org.name) + end) + elseif fields.remove_org then + local new_request = { + user = pname, + type = "delete", + params = {} + } + org:make_request(new_request) + modpol.interactions.org_dashboard(pname,org.name) + elseif fields.back then + modpol.interactions.dashboard(pname) + + -- Put all dropdowns at the end + -- Receiving modules + elseif fields.modules + and fields.modules ~= "View..." then + local module = fields.modules + org:call_module(module, user) + + -- Receiving actions + elseif fields.actions + and fields.actions ~= "View..." then + local action = string.match( + fields.actions,"%[(%d)%]") + local process = org.processes[tonumber(action)] + if process then + org:interact(process, user) + 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.policy_dashboard +-- input: user (string), org_id (int), policy (string) +-- output: opens a dashboard for viewing/editing policy details +-- TODO +function modpol.interactions.policy_dashboard( + user, org_id, policy) + modpol.interactions.message( + user, + "Not yet implemented: " .. policy) +end + + +-- BASIC INTERACTION FUNCTIONS +-- =========================== + +-- Function: modpol.interactions.message +-- input: message (string) +-- output +function modpol.interactions.message(user, message) + minetest.chat_send_player(user, message) +end + +-- Function: modpol.interactions.text_query +-- Overrides function at modpol/interactions.lua +-- input: user (string), query (string), func (function) +-- func input: user input (string) +-- output: Applies "func" to user input +function modpol.interactions.text_query(user, query, func) + -- set up formspec + local formspec = { + "formspec_version[4]", + "size[10,4]", + "label[0.5,1;", minetest.formspec_escape(query), "]", + "field[0.5,1.25;9,0.8;input;;]", + "button[0.5,2.5;1,0.8;yes;OK]", + } + local formspec_string = table.concat(formspec, "") + -- present to players + 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;"..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 ~= "cancel" then + local choice = fields.input + local func = _contexts[pname]["dropdown_query_func"] + if not choice then + -- no choice, do nothing + elseif func then + func(choice) + else + modpol.interactions.message(pname, "dropdown_query: " .. choice) + end + end + minetest.close_formspec(pname, formname) + end +end) + + +-- SECONDARY INTERACTIONS +-- ====================== + +-- 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[5,3]", + "label[0.375,0.5;",minetest.formspec_escape(question), "]", + "button[1,1.5;1,0.8;yes;Yes]", + "button[2,1.5;1,0.8;no;No]", + --TKTK can we enable text wrapping? + --TKTK 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 + modpol.interactions.message(pname, "Responded " .. vote) + local func = _contexts[pname]["binary_poll_func"] + if func then func(vote) end + end + minetest.close_formspec(pname, formname) + end +end) + +-- COMPLEX INTERACTIONS +-- ==================== + +-- Function: modpol.interactions.message_org +-- input: initiator (string), org_id (number), message (string) +-- output: broadcasts message to all org members +function modpol.interactions.message_org(initiator, org_id, message) + local org = modpol.orgs.get_org(org_id) + local users = org:list_members() + for k,v in ipairs(users) do + modpol.interactions.message(v, message) + end +end + + +-- Function: modpol.interactions.binary_poll_org +-- input: initator (user string), org_id (number) +-- output: gets question from initiator, asks all org members, broadcasts answers +-- TODO for testing. This should be implemented as a request. +function modpol.interactions.binary_poll_org(initiator, org_id, func) + local org = modpol.orgs.get_org(org_id) + local users = org:list_members() + modpol.interactions.text_query( + initiator, "Yes/no poll question:", + function(input) + for k,v in ipairs(users) do + modpol.interactions.binary_poll_user(v, input, func) + end + end) +end + +-- Function: modpol.interactions.add_org +-- input: initator (user string), base_org_id (ID) +-- output: interaction begins +-- GODMODE +function modpol.interactions.add_org(user, base_org_id) + modpol.interactions.text_query( + user,"Org name:", + function(input) + local base_org = modpol.orgs.get_org(1) + local result = base_org:add_org(input, user) + local message = input .. " created" + modpol.interactions.message(user, message) + modpol.interactions.dashboard(user) + end) +end + +-- Function: modpol.interactions.remove_org +-- input: initator (user string) +-- output: interaction begins +-- GODMODE +function modpol.interactions.remove_org(user) + -- start formspec + local orgs_list = modpol.orgs.list_all() + local label = "Choose an org to remove:" + modpol.interactions.dropdown_query( + user, label, orgs_list, + function(input) + if input then + local target_org = modpol.orgs.get_org(input) + local result = target_org:delete() + local message = input .. " deleted" + modpol.interactions.message(user, message) + end + modpol.interactions.dashboard(user) + end) +end diff --git a/modpol_minetest/overrides/users.lua b/modpol_minetest/overrides/users.lua new file mode 100644 index 0000000..229ebfb --- /dev/null +++ b/modpol_minetest/overrides/users.lua @@ -0,0 +1,21 @@ + +-- =================================================================== +-- Function: modpol.list_users(org) +-- Overwrites function at /users.lua +-- Params: +-- if nil, lists instance members; if an org name, lists its members +-- Output: a table with names of players currently in the game +modpol.list_users = function(org) + local users = {} + if (org == nil) then -- no specified org; all players + for _,player in ipairs(minetest.get_connected_players()) do + local name = player:get_player_name() + table.insert(users,name) + end + else -- if an org is specified + if (modpol.orgs[org] ~= nil) then -- org exists + users = modpol.orgs[org]["members"] + end + end + return users + end \ No newline at end of file