interactions.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. --- INTERACTIONS.LUA (CLI).
  2. -- User interaction functions for Modular Politics
  3. -- Called by modpol.lua
  4. -- @module modpol.interactions
  5. modpol.interactions = {}
  6. -- UTILITIES
  7. -- =========
  8. --- Output: returns a string listing a module's policies
  9. -- @function modpol.interactions.get_policy_string
  10. -- @param org (string or number) name or id for the org
  11. -- @param module_slug (string)
  12. -- @param sep (string) separator string
  13. function modpol.interactions.get_policy_string(
  14. org, module_slug, sep)
  15. local this_org = modpol.orgs.get_org(org)
  16. local this_module = modpol.modules[module_slug]
  17. local output = {}
  18. if not this_org.policies[module_slug] then
  19. return "Module not in org"
  20. elseif modpol.util.num_pairs(this_module.config) > 0 then
  21. for k, v in pairs(this_module.config) do
  22. local this_policy = "[Policy error]"
  23. if this_org.policies[module_slug][k] then
  24. this_policy =
  25. tostring(this_org.policies[module_slug][k])
  26. else
  27. if not v then
  28. this_policy = "none"
  29. else
  30. this_policy = tostring(v)
  31. end
  32. end
  33. table.insert(output, k.." - "..this_policy)
  34. end
  35. return "Policies:\n" .. table.concat(output, sep)
  36. else
  37. return "No policies"
  38. end
  39. end
  40. -- DASHBOARDS
  41. -- ==========
  42. --- Output: Display a menu of commands to the user
  43. -- @function modpol.interactions.dashboard
  44. -- @param user (string)
  45. -- Q: Should this return a menu of commands relevant to the specific user?
  46. -- TKTK currently just prints all of modpol---needs major improvement
  47. function modpol.interactions.dashboard(user)
  48. -- adds user to root org if not already in it
  49. if not modpol.instance:has_member(user) then
  50. modpol.instance:add_member(user)
  51. end
  52. local all_users = modpol.instance:list_members()
  53. print('\n-=< MODPOL DASHBOARD >=-')
  54. print('All orgs: (user orgs indicated by *)')
  55. for id, org in ipairs(modpol.orgs.array) do
  56. if type(org) == "table" then
  57. local indicator = ""
  58. if org:has_member(user) then indicator = "*" end
  59. print('['..id..'] '..indicator..org.name)
  60. end
  61. end
  62. -- pending list
  63. local user_pending = {}
  64. local user_pending_count = 0
  65. for k,v in ipairs(modpol.orgs.array) do
  66. if v.pending and v.pending[user] then
  67. if modpol.util.num_pairs(v.pending[user]) ~= 0 then
  68. table.insert(user_pending, v.name)
  69. user_pending_count = user_pending_count + 1
  70. end
  71. end
  72. end
  73. print('Pending actions in: '..table.concat(user_pending,', '))
  74. print('All users: ' .. table.concat(all_users, ', '))
  75. print()
  76. print("Commands: (O)rg, (U)ser, (R)eset, (Q)uit")
  77. local sel = io.read()
  78. if sel == "O" or sel == "o" then
  79. print('Access which org id?')
  80. sel = io.read()
  81. print()
  82. if modpol.orgs.array[tonumber(sel)] then
  83. local sel_org = modpol.orgs.array[tonumber(sel)].name
  84. modpol.interactions.org_dashboard(user, sel_org)
  85. else
  86. print("Org id not found")
  87. modpol.interactions.dashboard(user)
  88. end
  89. elseif sel == "U" or sel == "u" then
  90. print("Access which user?")
  91. sel = io.read()
  92. print()
  93. if modpol.instance:has_member(sel) then
  94. modpol.interactions.user_dashboard(
  95. user, sel,
  96. function()
  97. modpol.interactions.dashboard(user)
  98. end
  99. )
  100. else
  101. print("User name not found")
  102. modpol.interactions.dashboard(user)
  103. end
  104. elseif sel == "R" or sel == "r" then
  105. modpol.instance.members = {}
  106. modpol.orgs.reset()
  107. print("Orgs and users reset")
  108. modpol.interactions.dashboard(user)
  109. elseif sel == "Q" or sel == "q" then
  110. return
  111. else
  112. print("Invalid input, try again")
  113. modpol.interactions.dashboard(user)
  114. end
  115. end
  116. --- Output: Displays a menu of org-specific commands to the user
  117. -- @function modpol.interactions.org_dashboard
  118. -- @param user (string)
  119. -- @param org_string (string or id)
  120. function modpol.interactions.org_dashboard(user, org_string)
  121. local org = modpol.orgs.get_org(org_string)
  122. if not org then return nil end
  123. -- identify parent
  124. local parent = ""
  125. if org.id == 1 then
  126. parent = "none"
  127. else
  128. parent = modpol.orgs.get_org(org.parent).name
  129. end
  130. -- identify children
  131. local children = {}
  132. for k,v in ipairs(org.children) do
  133. local this_child = modpol.orgs.get_org(v)
  134. if this_child then
  135. table.insert(children, this_child.name)
  136. end
  137. end
  138. -- prepare modules menu
  139. local modules = {}
  140. if modpol.modules then
  141. for k,v in pairs(modpol.modules) do
  142. if not v.hide and -- hide utility modules
  143. org.policies[k] then -- org includes it
  144. local module_entry = v.slug
  145. table.insert(modules, module_entry)
  146. end
  147. end
  148. end
  149. table.sort(modules)
  150. -- list pending
  151. local processes = {}
  152. for i,v in ipairs(org.processes) do
  153. if v ~= "deleted" then
  154. processes[i] = org.processes[i]
  155. end
  156. end
  157. local process_msg = #processes .. " total processes"
  158. if org.pending[user] then
  159. process_msg = process_msg .. " (" ..
  160. modpol.util.num_pairs(org.pending[user]) .. " pending)"
  161. else
  162. process_msg = process_msg .. " (0 pending)"
  163. end
  164. -- set up output
  165. print('\n-=< ORG DASHBOARD >=-')
  166. print("Org: " .. org.name)
  167. print("Parent: " .. parent)
  168. print("Members: " .. table.concat(org.members, ", "))
  169. print("Child orgs: " .. table.concat(children, ", "))
  170. print("Modules: " .. table.concat(modules, ", "))
  171. print("Pending: " .. process_msg)
  172. print()
  173. print("Commands: (M)odules, (P)ending, (B)ack")
  174. local sel = io.read()
  175. print()
  176. if sel == 'm' or sel == 'M' then
  177. print("Type module name: ")
  178. local module_sel = io.read()
  179. print()
  180. local module_result = false
  181. for k,v in ipairs(modules) do
  182. if v == module_sel then
  183. module_result = true
  184. end
  185. end
  186. local module = modpol.modules[module_sel]
  187. if module_result then
  188. modpol.interactions.binary_poll_user(
  189. user,
  190. module.name..":\n"..
  191. module.desc.."\n"..
  192. modpol.interactions.get_policy_string(
  193. org.name, module.slug, "\n")..
  194. "\n".."Proceed?",
  195. function(input)
  196. print("\n")
  197. if input == "Yes" then
  198. org:call_module(module_sel, user)
  199. elseif input == "No" then
  200. modpol.interactions.org_dashboard(
  201. pname, org.id)
  202. end
  203. end)
  204. else
  205. print("Error: Module not found.")
  206. modpol.interactions.org_dashboard(user, org.id)
  207. end
  208. elseif sel == 'p' or sel == 'P' then -- Pending processes
  209. print("All processes: (* indicates pending)")
  210. for i,v in ipairs(processes) do
  211. local active = ''
  212. if org.pending[user] then
  213. if org.pending[user][v.id] then
  214. active = '*'
  215. end
  216. end
  217. print("["..v.id.."] "..v.slug..active)
  218. end
  219. print()
  220. print("Interact with which one (use [id] number)?")
  221. local to_interact = io.read()
  222. local process = org.processes[tonumber(to_interact)]
  223. if not process then
  224. modpol.interactions.message(
  225. user, "Not a pending process")
  226. modpol.interactions.org_dashboard(user, org.id)
  227. return
  228. end
  229. if org:has_pending_actions(user) then
  230. if org.pending[user][process.id] then
  231. org:interact(process.id, user)
  232. end
  233. modpol.interactions.org_dashboard(user, org.id)
  234. end
  235. elseif sel == 'b' or sel == 'B' then
  236. modpol.interactions.dashboard(user)
  237. else
  238. print("Command not found")
  239. modpol.interactions.org_dashboard(user, org.name)
  240. end
  241. end
  242. --- Function: modpol.interactions.user_dashboard
  243. -- Displays a dashboard about a particular user
  244. -- @param viewer Name of user viewing the dashboard (string)
  245. -- @param user Name of user being viewed (string)
  246. -- @param completion Optional function to call on Done button
  247. function modpol.interactions.user_dashboard(viewer, user, completion)
  248. local user_orgs = {}
  249. local user_modules = {}
  250. print("\n-=< USER DASHBOARD: "..user.." >=-")
  251. print("User's orgs:")
  252. for id, org in ipairs(modpol.orgs.array) do
  253. if type(org) == "table" then
  254. if org:has_member(user) then
  255. print(org.name)
  256. end
  257. end
  258. end
  259. print()
  260. print("Commands: (M)essage user, Enter when done")
  261. local sel = io.read()
  262. if sel == "M" or sel == "m" then
  263. modpol.interactions.message_user(
  264. viewer, user)
  265. completion()
  266. else
  267. completion()
  268. end
  269. end
  270. -- INTERACTION PRIMITIVES
  271. -- ======================
  272. --- Prints message to CLI.
  273. -- Buttons: message, done
  274. -- @function modpol.interactions.message
  275. -- @param user (string)
  276. -- @param message (string)
  277. function modpol.interactions.message(user, message)
  278. print(user .. ": " .. message)
  279. end
  280. --- Gets and sends a message from one user to another
  281. -- @function modpol.interactions.message_user
  282. -- @param sender Name of user sending (string)
  283. -- @param recipient Name of user receiving (string)
  284. function modpol.interactions.message_user(sender, recipient)
  285. print("Enter your message for "..recipient..":")
  286. local sel = io.read()
  287. modpol.interactions.message(
  288. recipient,
  289. sel.." [from "..sender.."]")
  290. end
  291. --- Displays complex data to a user
  292. -- @function modpol.interactions.display
  293. -- @param user Name of target user (string)
  294. -- @param title Title of display (string)
  295. -- @param message Content of message (string or table of strings)
  296. -- @param done Optional function for what happens when user is done
  297. function modpol.interactions.display(user, title, message, completion)
  298. local output = ""
  299. output = "\n-=< "..title.." >=-\n\n"
  300. if type(message) == "table" then
  301. output = table.concat(message,"\n")
  302. elseif type(message) == "string" then
  303. output = message
  304. elseif type(message) == "number" then
  305. output = message
  306. else
  307. modpol.interactions.message(
  308. self.initiator, "Error: message not typed for display")
  309. modpol.interactions.message(
  310. self.initiator, "Error: input not typed for display")
  311. if completion then completion() else
  312. modpol.interactions.dashboard(user)
  313. end
  314. end
  315. print(message)
  316. print("\nEnter to continue")
  317. io.read()
  318. if completion then completion() else
  319. modpol.interactions.dashboard(user)
  320. end
  321. end
  322. --- Applies "func" to user input.
  323. -- Func input: user input (string)
  324. -- @function modpol.interactions.text_query
  325. -- @param User (string)
  326. -- @param Query (string)
  327. -- @param func (function)
  328. function modpol.interactions.text_query(user, query, func)
  329. print(user .. ": " .. query)
  330. answer = io.read()
  331. func(answer)
  332. end
  333. --- Output: Calls func on choice.
  334. -- Func input: choice (string)
  335. -- @function modpol.interactions.dropdown_query
  336. -- @param user (string)
  337. -- @param label (string)
  338. -- @param options (table of strings)
  339. -- @param func (choice) (function)
  340. function modpol.interactions.dropdown_query(user, label, options, func)
  341. -- set up options
  342. local options_display = ""
  343. local options_number = 0
  344. for k,v in ipairs(options) do
  345. options_display = options_display .. k .. ". " ..
  346. options[k] .. "\n"
  347. options_number = options_number + 1
  348. end
  349. options_display = options_display .. "Select number:"
  350. if options_number == 0 then
  351. print("Error: No options given for dropdown")
  352. return nil
  353. end
  354. -- begin displaying
  355. print(user .. ": " .. label)
  356. print(options_display)
  357. -- read input and produce output
  358. local answer
  359. answer = io.read()
  360. answer = tonumber(answer)
  361. if answer then
  362. if answer >= 1 and answer <= options_number then
  363. print("Selection: " .. options[answer])
  364. func(options[answer])
  365. else
  366. print("Error: Not in dropdown range")
  367. return nil
  368. end
  369. else
  370. print("Error: Must be a number")
  371. return nil
  372. end
  373. end
  374. --- Allows user to select from a set of options
  375. -- @function modpol.interactions.checkbox_query
  376. -- @param user Name of user (string)
  377. -- @param label Query for user before options (string)
  378. -- @param options Table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
  379. -- @param func Function to be called with param "input", made up of the corrected table in the same format as the param options
  380. function modpol.interactions.checkbox_query(
  381. user, label, options, func)
  382. -- set up options
  383. local options_display = ""
  384. local options_number = 0
  385. for i,v in ipairs(options) do
  386. local checked = false
  387. if v[2] then checked = true end
  388. if checked then
  389. checked = "x"
  390. else
  391. checked = " "
  392. end
  393. options_display = options_display..i..". ["..
  394. checked.."] "..v[1].."\n"
  395. options_number = options_number + 1
  396. end
  397. if options_number == 0 then
  398. print("Error: No options given for dropdown")
  399. return nil
  400. end
  401. options_display = options_display..
  402. "List comma-separated options to flip (e.g., 1,2,5):"
  403. -- begin displaying
  404. print(user .. ": " .. label)
  405. print(options_display)
  406. -- read input and produce output
  407. local answer = io.read()
  408. local answer_table = {}
  409. for match in (answer..","):gmatch("(.-)"..",") do
  410. table.insert(answer_table, tonumber(match))
  411. end
  412. local result_table = modpol.util.copy_table(options)
  413. for i,v in ipairs(answer_table) do
  414. if result_table[v] then
  415. -- flip the boolean on selected options
  416. result_table[v][2] = not result_table[v][2]
  417. end
  418. end
  419. func(result_table)
  420. end
  421. --- Output: Applies "func" to user input.
  422. -- Func input: user input (string: y/n)
  423. -- @function modpol.interactions.binary_poll_user
  424. -- @param user (string)
  425. -- @param question (string)
  426. -- @param func (function)
  427. function modpol.interactions.binary_poll_user(user, question, func)
  428. local query = "Poll for " .. user .. " (y/n): ".. question
  429. local answer
  430. repeat
  431. print(query)
  432. answer = io.read()
  433. until answer == "y" or answer == "n"
  434. if answer == "y" then
  435. func("Yes")
  436. elseif answer == "n" then
  437. func("No")
  438. else
  439. modpol.interactions.message(user, "Error: invalid response")
  440. end
  441. end
  442. -- COMPLEX INTERACTIONS
  443. -- ====================
  444. --- Output: broadcasts message to all org members
  445. -- @function modpol.interactions.message_org
  446. -- @param initiator (string)
  447. -- @param org (number or string)
  448. -- @param message (string)
  449. function modpol.interactions.message_org(initiator, org, message)
  450. local this_org = modpol.orgs.get_org(org)
  451. local users = this_org:list_members()
  452. for k,v in ipairs(users) do
  453. modpol.interactions.message(v, message)
  454. end
  455. end
  456. -- Function: modpol.interactions.binary_poll_org
  457. -- input: initator (user string), org_id (number)
  458. -- output: gets question from initiator, asks all org members, broadcasts answers
  459. -- TESTING
  460. --testing command
  461. function modpol.msg(text)
  462. modpol.interactions.message("TEST MSG",text)
  463. end