interactions.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  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 "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. table.insert(children, this_child.name)
  135. end
  136. -- prepare modules menu
  137. local modules = {}
  138. if modpol.modules then
  139. for k,v in pairs(modpol.modules) do
  140. if not v.hide and -- hide utility modules
  141. org.policies[k] then -- org includes it
  142. local module_entry = v.slug
  143. table.insert(modules, module_entry)
  144. end
  145. end
  146. end
  147. table.sort(modules)
  148. -- list pending
  149. local process_msg = modpol.util.lazy_table_length(org.processes, "deleted") .. " total processes"
  150. if org.pending[user] then
  151. process_msg = process_msg .. " (" ..
  152. modpol.util.num_pairs(org.pending[user]) .. " pending)"
  153. else
  154. process_msg = process_msg .. " (0 pending)"
  155. end
  156. -- set up output
  157. print('\n-=< ORG DASHBOARD >=-')
  158. print("Org: " .. org.name)
  159. print("Parent: " .. parent)
  160. print("Members: " .. table.concat(org.members, ", "))
  161. print("Child orgs: " .. table.concat(children, ", "))
  162. print("Modules: " .. table.concat(modules, ", "))
  163. print("Pending: " .. process_msg)
  164. print()
  165. print("Commands: (M)odules, (P)ending, (B)ack")
  166. local sel = io.read()
  167. print()
  168. if sel == 'm' or sel == 'M' then
  169. print("Type module name: ")
  170. local module_sel = io.read()
  171. print()
  172. local module_result = false
  173. for k,v in ipairs(modules) do
  174. if v == module_sel then
  175. module_result = true
  176. end
  177. end
  178. local module = modpol.modules[module_sel]
  179. if module_result then
  180. modpol.interactions.binary_poll_user(
  181. user,
  182. module.name..":\n"..
  183. module.desc.."\n"..
  184. modpol.interactions.get_policy_string(
  185. org.name, module.slug, "\n")..
  186. "\n".."Proceed?",
  187. function(input)
  188. print("\n")
  189. if input == "Yes" then
  190. org:call_module(module_sel, user)
  191. elseif input == "No" then
  192. modpol.interactions.org_dashboard(
  193. pname, org.id)
  194. end
  195. end)
  196. else
  197. print("Error: Module not found.")
  198. modpol.interactions.org_dashboard(user, org.id)
  199. end
  200. elseif sel == 'p' or sel == 'P' then -- Pending processes
  201. print("All processes: (* indicates pending)")
  202. for i,v in ipairs(processes) do
  203. local active = ''
  204. if org.pending[user] then
  205. if org.pending[user][v.id] then
  206. active = '*'
  207. end
  208. end
  209. print("["..v.id.."] "..v.slug..active)
  210. end
  211. print()
  212. print("Interact with which one (use [id] number)?")
  213. local to_interact = io.read()
  214. local process = org.processes[tonumber(to_interact)]
  215. if not process then
  216. modpol.interactions.message(
  217. user, "Not a pending process")
  218. modpol.interactions.org_dashboard(user, org.id)
  219. return
  220. end
  221. if org:has_pending_actions(user) then
  222. if org.pending[user][process.id] then
  223. org:interact(process.id, user)
  224. end
  225. modpol.interactions.org_dashboard(user, org.id)
  226. end
  227. elseif sel == 'b' or sel == 'B' then
  228. modpol.interactions.dashboard(user)
  229. else
  230. print("Command not found")
  231. modpol.interactions.org_dashboard(user, org.name)
  232. end
  233. end
  234. --- Function: modpol.interactions.user_dashboard
  235. -- Displays a dashboard about a particular user
  236. -- @param viewer Name of user viewing the dashboard (string)
  237. -- @param user Name of user being viewed (string)
  238. -- @param completion Optional function to call on Done button
  239. function modpol.interactions.user_dashboard(viewer, user, completion)
  240. local user_orgs = {}
  241. local user_modules = {}
  242. print("\n-=< USER DASHBOARD: "..user.." >=-")
  243. print("User's orgs:")
  244. for id, org in ipairs(modpol.orgs.array) do
  245. if type(org) == "table" then
  246. if org:has_member(user) then
  247. print(org.name)
  248. end
  249. end
  250. end
  251. print()
  252. print("Commands: (M)essage user, Enter when done")
  253. local sel = io.read()
  254. if sel == "M" or sel == "m" then
  255. modpol.interactions.message_user(
  256. viewer, user)
  257. completion()
  258. else
  259. completion()
  260. end
  261. end
  262. -- INTERACTION PRIMITIVES
  263. -- ======================
  264. --- Prints message to CLI.
  265. -- Buttons: message, done
  266. -- @function modpol.interactions.message
  267. -- @param user (string)
  268. -- @param message (string)
  269. function modpol.interactions.message(user, message)
  270. print(user .. ": " .. message)
  271. end
  272. --- Gets and sends a message from one user to another
  273. -- @function modpol.interactions.message_user
  274. -- @param sender Name of user sending (string)
  275. -- @param recipient Name of user receiving (string)
  276. function modpol.interactions.message_user(sender, recipient)
  277. print("Enter your message for "..recipient..":")
  278. local sel = io.read()
  279. modpol.interactions.message(
  280. recipient,
  281. sel.." [from "..sender.."]")
  282. end
  283. --- Displays complex data to a user
  284. -- @function modpol.interactions.display
  285. -- @param user Name of target user (string)
  286. -- @param title Title of display (string)
  287. -- @param message Content of message (string or table of strings)
  288. -- @param done Optional function for what happens when user is done
  289. function modpol.interactions.display(user, title, message, completion)
  290. local output = ""
  291. output = "\n-=< "..title.." >=-\n\n"
  292. if type(message) == "table" then
  293. output = table.concat(message,"\n")
  294. elseif type(message) == "string" then
  295. output = message
  296. elseif type(message) == "number" then
  297. output = message
  298. else
  299. modpol.interactions.message(
  300. self.initiator, "Error: message not typed for display")
  301. modpol.interactions.message(
  302. self.initiator, "Error: input not typed for display")
  303. if completion then completion() else
  304. modpol.intereactions.dashboard(user)
  305. end
  306. end
  307. print(message)
  308. print("\nEnter to continue")
  309. io.read()
  310. if completion then completion() else
  311. modpol.intereactions.dashboard(user)
  312. end
  313. end
  314. --- Applies "func" to user input.
  315. -- Func input: user input (string)
  316. -- @function modpol.interactions.text_query
  317. -- @param User (string)
  318. -- @param Query (string)
  319. -- @param func (function)
  320. function modpol.interactions.text_query(user, query, func)
  321. print(user .. ": " .. query)
  322. answer = io.read()
  323. func(answer)
  324. end
  325. --- Output: Calls func on choice.
  326. -- Func input: choice (string)
  327. -- @function modpol.interactions.dropdown_query
  328. -- @param user (string)
  329. -- @param label (string)
  330. -- @param options (table of strings)
  331. -- @param func (choice) (function)
  332. function modpol.interactions.dropdown_query(user, label, options, func)
  333. -- set up options
  334. local options_display = ""
  335. local options_number = 0
  336. for k,v in ipairs(options) do
  337. options_display = options_display .. k .. ". " ..
  338. options[k] .. "\n"
  339. options_number = options_number + 1
  340. end
  341. options_display = options_display .. "Select number:"
  342. if options_number == 0 then
  343. print("Error: No options given for dropdown")
  344. return nil
  345. end
  346. -- begin displaying
  347. print(user .. ": " .. label)
  348. print(options_display)
  349. -- read input and produce output
  350. local answer
  351. answer = io.read()
  352. answer = tonumber(answer)
  353. if answer then
  354. if answer >= 1 and answer <= options_number then
  355. print("Selection: " .. options[answer])
  356. func(options[answer])
  357. else
  358. print("Error: Not in dropdown range")
  359. return nil
  360. end
  361. else
  362. print("Error: Must be a number")
  363. return nil
  364. end
  365. end
  366. --- Allows user to select from a set of options
  367. -- @function modpol.interactions.checkbox_query
  368. -- @param user Name of user (string)
  369. -- @param label Query for user before options (string)
  370. -- @param options Table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
  371. -- @param func Function to be called with param "input", made up of the corrected table in the same format as the param options
  372. function modpol.interactions.checkbox_query(
  373. user, label, options, func)
  374. -- set up options
  375. local options_display = ""
  376. local options_number = 0
  377. for i,v in ipairs(options) do
  378. local checked = false
  379. if v[2] then checked = true end
  380. if checked then
  381. checked = "x"
  382. else
  383. checked = " "
  384. end
  385. options_display = options_display..i..". ["..
  386. checked.."] "..v[1].."\n"
  387. options_number = options_number + 1
  388. end
  389. if options_number == 0 then
  390. print("Error: No options given for dropdown")
  391. return nil
  392. end
  393. options_display = options_display..
  394. "List comma-separated options to flip (e.g., 1,2,5):"
  395. -- begin displaying
  396. print(user .. ": " .. label)
  397. print(options_display)
  398. -- read input and produce output
  399. local answer = io.read()
  400. local answer_table = {}
  401. for match in (answer..","):gmatch("(.-)"..",") do
  402. table.insert(answer_table, tonumber(match))
  403. end
  404. local result_table = modpol.util.copy_table(options)
  405. for i,v in ipairs(answer_table) do
  406. if result_table[v] then
  407. -- flip the boolean on selected options
  408. result_table[v][2] = not result_table[v][2]
  409. end
  410. end
  411. func(result_table)
  412. end
  413. --- Output: Applies "func" to user input.
  414. -- Func input: user input (string: y/n)
  415. -- @function modpol.interactions.binary_poll_user
  416. -- @param user (string)
  417. -- @param question (string)
  418. -- @param func (function)
  419. function modpol.interactions.binary_poll_user(user, question, func)
  420. local query = "Poll for " .. user .. " (y/n): ".. question
  421. local answer
  422. repeat
  423. print(query)
  424. answer = io.read()
  425. until answer == "y" or answer == "n"
  426. if answer == "y" then
  427. func("Yes")
  428. elseif answer == "n" then
  429. func("No")
  430. else
  431. modpol.interactions.message(user, "Error: invalid response")
  432. end
  433. end
  434. -- COMPLEX INTERACTIONS
  435. -- ====================
  436. --- Output: broadcasts message to all org members
  437. -- @function modpol.interactions.message_org
  438. -- @param initiator (string)
  439. -- @param org (number or string)
  440. -- @param message (string)
  441. function modpol.interactions.message_org(initiator, org, message)
  442. local this_org = modpol.orgs.get_org(org)
  443. local users = this_org:list_members()
  444. for k,v in ipairs(users) do
  445. modpol.interactions.message(v, message)
  446. end
  447. end
  448. -- Function: modpol.interactions.binary_poll_org
  449. -- input: initator (user string), org_id (number)
  450. -- output: gets question from initiator, asks all org members, broadcasts answers
  451. -- TESTING
  452. --testing command
  453. function modpol.msg(text)
  454. modpol.interactions.message("TEST MSG",text)
  455. end