--- 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 if not v then this_policy = "none" else this_policy = tostring(v) end 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 sel == "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 processes = {} for i,v in ipairs(org.processes) do if v ~= "deleted" then processes[i] = org.processes[i] end end local process_msg = #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) print("\n") 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 -- Pending processes print("All processes: (* indicates pending)") for i,v in ipairs(processes) do local active = '' if org.pending[user] then if org.pending[user][v.id] then active = '*' end end print("["..v.id.."] "..v.slug..active) 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 modpol.interactions.org_dashboard(user, org.id) 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.interactions.dashboard(user) end end print(message) print("\nEnter to continue") io.read() if completion then completion() else modpol.interactions.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 func("Yes") elseif answer == "n" then 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