interactions.lua 15 KB

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