modpol/modpol_core/interactions/interactions.lua
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

507 lines
15 KiB
Lua

--- INTERACTIONS.LUA (CLI).
-- User interaction functions for Modular Politics
-- Called by modpol.lua
-- @module modpol.interactions
modpol.interactions = {}
-- UTILITIES
-- =========
--- Output: returns a string listing a module's policies
-- @function modpol.interactions.get_policy_string
-- @param org (string or number) name or id for the org
-- @param module_slug (string)
-- @param sep (string) separator string
function modpol.interactions.get_policy_string(
org, module_slug, sep)
local this_org = modpol.orgs.get_org(org)
local this_module = modpol.modules[module_slug]
local output = {}
if not this_org.policies[module_slug] then
return "Module not in org"
elseif modpol.util.num_pairs(this_module.config) > 0 then
for k, v in pairs(this_module.config) do
local this_policy = "[Policy error]"
if this_org.policies[module_slug][k] then
this_policy =
tostring(this_org.policies[module_slug][k])
else
this_policy = tostring(v)
end
table.insert(output, k.." - "..this_policy)
end
return "Policies:\n" .. table.concat(output, sep)
else
return "No policies"
end
end
-- DASHBOARDS
-- ==========
--- Output: Display a menu of commands to the user
-- @function modpol.interactions.dashboard
-- @param user (string)
-- Q: Should this return a menu of commands relevant to the specific user?
-- TKTK currently just prints all of modpol---needs major improvement
function modpol.interactions.dashboard(user)
-- adds user to root org if not already in it
if not modpol.instance:has_member(user) then
modpol.instance:add_member(user)
end
local all_users = modpol.instance:list_members()
print('\n-=< MODPOL DASHBOARD >=-')
print('All orgs: (user orgs indicated by *)')
for id, org in ipairs(modpol.orgs.array) do
if type(org) == "table" then
local indicator = ""
if org:has_member(user) then indicator = "*" end
print('['..id..'] '..indicator..org.name)
end
end
-- pending list
local user_pending = {}
local user_pending_count = 0
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
print('Pending actions in: '..table.concat(user_pending,', '))
print('All users: ' .. table.concat(all_users, ', '))
print()
print("Commands: (O)rg, (U)ser, (R)eset, (Q)uit")
local sel = io.read()
if sel == "O" or sel == "o" then
print('Access which org id?')
sel = io.read()
print()
if modpol.orgs.array[tonumber(sel)] then
local sel_org = modpol.orgs.array[tonumber(sel)].name
modpol.interactions.org_dashboard(user, sel_org)
else
print("Org id not found")
modpol.interactions.dashboard(user)
end
elseif sel == "U" or sel == "u" then
print("Access which user?")
sel = io.read()
print()
if modpol.instance:has_member(sel) then
modpol.interactions.user_dashboard(
user, sel,
function()
modpol.interactions.dashboard(user)
end
)
else
print("User name not found")
modpol.interactions.dashboard(user)
end
elseif sel == "R" or sel == "r" then
modpol.instance.members = {}
modpol.orgs.reset()
print("Orgs and users reset")
modpol.interactions.dashboard(user)
elseif sel == "Q" or "q" then
return
else
print("Invalid input, try again")
modpol.interactions.dashboard(user)
end
end
--- Output: Displays a menu of org-specific commands to the user
-- @function modpol.interactions.org_dashboard
-- @param user (string)
-- @param org_string (string or id)
function modpol.interactions.org_dashboard(user, org_string)
local org = modpol.orgs.get_org(org_string)
if not org then return nil end
-- identify parent
local parent = ""
if org.id == 1 then
parent = "none"
else
parent = modpol.orgs.get_org(org.parent).name
end
-- identify children
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
-- 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.slug
table.insert(modules, module_entry)
end
end
end
table.sort(modules)
-- list pending
local process_msg = #org.processes .. " total processes"
if org.pending[user] then
process_msg = process_msg .. " (" ..
modpol.util.num_pairs(org.pending[user]) .. " pending)"
else
process_msg = process_msg .. " (0 pending)"
end
-- set up output
print('\n-=< ORG DASHBOARD >=-')
print("Org: " .. org.name)
print("Parent: " .. parent)
print("Members: " .. table.concat(org.members, ", "))
print("Child orgs: " .. table.concat(children, ", "))
print("Modules: " .. table.concat(modules, ", "))
print("Pending: " .. process_msg)
print()
print("Commands: (M)odules, (P)ending, (B)ack")
local sel = io.read()
print()
if sel == 'm' or sel == 'M' then
print("Type module name: ")
local module_sel = io.read()
print()
local module_result = false
for k,v in ipairs(modules) do
if v == module_sel then
module_result = true
end
end
local module = modpol.modules[module_sel]
if module_result then
modpol.interactions.binary_poll_user(
user,
module.name..":\n"..
module.desc.."\n"..
modpol.interactions.get_policy_string(
org.name, module.slug, "\n")..
"\n".."Proceed?",
function(input)
if input == "Yes" then
org:call_module(module_sel, user)
elseif input == "No" then
modpol.interactions.org_dashboard(
pname, org.id)
end
end)
else
print("Error: Module not found.")
modpol.interactions.org_dashboard(user, org.id)
end
elseif sel == 'p' or sel == 'P' then
local processes = {}
print("All processes: (* indicates pending)")
for i,v in ipairs(org.processes) do
if v ~= "deleted" then
local active = ''
if org.pending[user] then
if org.pending[user][v.id] then
active = '*'
end
end
print("["..v.id.."] "..v.slug..active)
end
end
print()
print("Interact with which one (use [id] number)?")
local to_interact = io.read()
local process = org.processes[tonumber(to_interact)]
if not process then
modpol.interactions.message(
user, "Not a pending process")
modpol.interactions.org_dashboard(user, org.id)
return
end
if org:has_pending_actions(user) then
if org.pending[user][process.id] then
org:interact(process.id, user)
end
end
elseif sel == 'b' or sel == 'B' then
modpol.interactions.dashboard(user)
else
print("Command not found")
modpol.interactions.org_dashboard(user, org.name)
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 = {}
local user_modules = {}
print("\n-=< USER DASHBOARD: "..user.." >=-")
print("User's orgs:")
for id, org in ipairs(modpol.orgs.array) do
if type(org) == "table" then
if org:has_member(user) then
print(org.name)
end
end
end
print()
print("Commands: (M)essage user, Enter when done")
local sel = io.read()
if sel == "M" or sel == "m" then
modpol.interactions.message_user(
viewer, user)
completion()
else
completion()
end
end
-- INTERACTION PRIMITIVES
-- ======================
--- Prints message to CLI.
-- Buttons: message, done
-- @function modpol.interactions.message
-- @param user (string)
-- @param message (string)
function modpol.interactions.message(user, message)
print(user .. ": " .. message)
end
--- Gets and sends a message from one user to another
-- @function modpol.interactions.message_user
-- @param sender Name of user sending (string)
-- @param recipient Name of user receiving (string)
function modpol.interactions.message_user(sender, recipient)
print("Enter your message for "..recipient..":")
local sel = io.read()
modpol.interactions.message(
recipient,
sel.." [from "..sender.."]")
end
--- Displays complex data to a user
-- @function modpol.interactions.display
-- @param user Name of target user (string)
-- @param title Title of display (string)
-- @param message Content of message (string or table of strings)
-- @param done Optional function for what happens when user is done
function modpol.interactions.display(user, title, message, completion)
local output = ""
output = "\n-=< "..title.." >=-\n\n"
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")
modpol.interactions.message(
self.initiator, "Error: input not typed for display")
if completion then completion() else
modpol.intereactions.dashboard(user)
end
end
print(message)
print("\nEnter to continue")
io.read()
if completion then completion() else
modpol.intereactions.dashboard(user)
end
end
--- Applies "func" to user input.
-- Func input: user input (string)
-- @function modpol.interactions.text_query
-- @param User (string)
-- @param Query (string)
-- @param func (function)
function modpol.interactions.text_query(user, query, func)
print(user .. ": " .. query)
answer = io.read()
func(answer)
end
--- Output: Calls func on choice.
-- Func input: choice (string)
-- @function modpol.interactions.dropdown_query
-- @param user (string)
-- @param label (string)
-- @param options (table of strings)
-- @param func (choice) (function)
function modpol.interactions.dropdown_query(user, label, options, func)
-- set up options
local options_display = ""
local options_number = 0
for k,v in ipairs(options) do
options_display = options_display .. k .. ". " ..
options[k] .. "\n"
options_number = options_number + 1
end
options_display = options_display .. "Select number:"
if options_number == 0 then
print("Error: No options given for dropdown")
return nil
end
-- begin displaying
print(user .. ": " .. label)
print(options_display)
-- read input and produce output
local answer
answer = io.read()
answer = tonumber(answer)
if answer then
if answer >= 1 and answer <= options_number then
print("Selection: " .. options[answer])
func(options[answer])
else
print("Error: Not in dropdown range")
return nil
end
else
print("Error: Must be a number")
return nil
end
end
--- Allows user to select from a set of options
-- @function modpol.interactions.checkbox_query
-- @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 options
local options_display = ""
local options_number = 0
for i,v in ipairs(options) do
local checked = false
if v[2] then checked = true end
if checked then
checked = "x"
else
checked = " "
end
options_display = options_display..i..". ["..
checked.."] "..v[1].."\n"
options_number = options_number + 1
end
if options_number == 0 then
print("Error: No options given for dropdown")
return nil
end
options_display = options_display..
"List comma-separated options to flip (e.g., 1,2,5):"
-- begin displaying
print(user .. ": " .. label)
print(options_display)
-- read input and produce output
local answer = io.read()
local answer_table = {}
for match in (answer..","):gmatch("(.-)"..",") do
table.insert(answer_table, tonumber(match))
end
local result_table = modpol.util.copy_table(options)
for i,v in ipairs(answer_table) do
if result_table[v] then
-- flip the boolean on selected options
result_table[v][2] = not result_table[v][2]
end
end
func(result_table)
end
--- Output: Applies "func" to user input.
-- Func input: user input (string: y/n)
-- @function modpol.interactions.binary_poll_user
-- @param user (string)
-- @param question (string)
-- @param func (function)
function modpol.interactions.binary_poll_user(user, question, func)
local query = "Poll for " .. user .. " (y/n): ".. question
local answer
repeat
print(query)
answer = io.read()
until answer == "y" or answer == "n"
if answer == "y" then
modpol.interactions.message(user, "Response recorded")
func("Yes")
elseif answer == "n" then
modpol.interactions.message(user, "Response recorded")
func("No")
else
modpol.interactions.message(user, "Error: invalid response")
end
end
-- COMPLEX INTERACTIONS
-- ====================
--- Output: broadcasts message to all org members
-- @function modpol.interactions.message_org
-- @param initiator (string)
-- @param org (number or string)
-- @param message (string)
function modpol.interactions.message_org(initiator, org, message)
local this_org = modpol.orgs.get_org(org)
local users = this_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
-- TESTING
--testing command
function modpol.msg(text)
modpol.interactions.message("TEST MSG",text)
end