interactions.lua 15 KB

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