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. 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. modpol.interactions.org_dashboard(user, org.id)
  224. end
  225. elseif sel == 'b' or sel == 'B' then
  226. modpol.interactions.dashboard(user)
  227. else
  228. print("Command not found")
  229. modpol.interactions.org_dashboard(user, org.name)
  230. end
  231. end
  232. --- Function: modpol.interactions.user_dashboard
  233. -- Displays a dashboard about a particular user
  234. -- @param viewer Name of user viewing the dashboard (string)
  235. -- @param user Name of user being viewed (string)
  236. -- @param completion Optional function to call on Done button
  237. function modpol.interactions.user_dashboard(viewer, user, completion)
  238. local user_orgs = {}
  239. local user_modules = {}
  240. print("\n-=< USER DASHBOARD: "..user.." >=-")
  241. print("User's orgs:")
  242. for id, org in ipairs(modpol.orgs.array) do
  243. if type(org) == "table" then
  244. if org:has_member(user) then
  245. print(org.name)
  246. end
  247. end
  248. end
  249. print()
  250. print("Commands: (M)essage user, Enter when done")
  251. local sel = io.read()
  252. if sel == "M" or sel == "m" then
  253. modpol.interactions.message_user(
  254. viewer, user)
  255. completion()
  256. else
  257. completion()
  258. end
  259. end
  260. -- INTERACTION PRIMITIVES
  261. -- ======================
  262. --- Prints message to CLI.
  263. -- Buttons: message, done
  264. -- @function modpol.interactions.message
  265. -- @param user (string)
  266. -- @param message (string)
  267. function modpol.interactions.message(user, message)
  268. print(user .. ": " .. message)
  269. end
  270. --- Gets and sends a message from one user to another
  271. -- @function modpol.interactions.message_user
  272. -- @param sender Name of user sending (string)
  273. -- @param recipient Name of user receiving (string)
  274. function modpol.interactions.message_user(sender, recipient)
  275. print("Enter your message for "..recipient..":")
  276. local sel = io.read()
  277. modpol.interactions.message(
  278. recipient,
  279. sel.." [from "..sender.."]")
  280. end
  281. --- Displays complex data to a user
  282. -- @function modpol.interactions.display
  283. -- @param user Name of target user (string)
  284. -- @param title Title of display (string)
  285. -- @param message Content of message (string or table of strings)
  286. -- @param done Optional function for what happens when user is done
  287. function modpol.interactions.display(user, title, message, completion)
  288. local output = ""
  289. output = "\n-=< "..title.." >=-\n\n"
  290. if type(message) == "table" then
  291. output = table.concat(message,"\n")
  292. elseif type(message) == "string" then
  293. output = message
  294. elseif type(message) == "number" then
  295. output = message
  296. else
  297. modpol.interactions.message(
  298. self.initiator, "Error: message not typed for display")
  299. modpol.interactions.message(
  300. self.initiator, "Error: input not typed for display")
  301. if completion then completion() else
  302. modpol.intereactions.dashboard(user)
  303. end
  304. end
  305. print(message)
  306. print("\nEnter to continue")
  307. io.read()
  308. if completion then completion() else
  309. modpol.intereactions.dashboard(user)
  310. end
  311. end
  312. --- Applies "func" to user input.
  313. -- Func input: user input (string)
  314. -- @function modpol.interactions.text_query
  315. -- @param User (string)
  316. -- @param Query (string)
  317. -- @param func (function)
  318. function modpol.interactions.text_query(user, query, func)
  319. print(user .. ": " .. query)
  320. answer = io.read()
  321. func(answer)
  322. end
  323. --- Output: Calls func on choice.
  324. -- Func input: choice (string)
  325. -- @function modpol.interactions.dropdown_query
  326. -- @param user (string)
  327. -- @param label (string)
  328. -- @param options (table of strings)
  329. -- @param func (choice) (function)
  330. function modpol.interactions.dropdown_query(user, label, options, func)
  331. -- set up options
  332. local options_display = ""
  333. local options_number = 0
  334. for k,v in ipairs(options) do
  335. options_display = options_display .. k .. ". " ..
  336. options[k] .. "\n"
  337. options_number = options_number + 1
  338. end
  339. options_display = options_display .. "Select number:"
  340. if options_number == 0 then
  341. print("Error: No options given for dropdown")
  342. return nil
  343. end
  344. -- begin displaying
  345. print(user .. ": " .. label)
  346. print(options_display)
  347. -- read input and produce output
  348. local answer
  349. answer = io.read()
  350. answer = tonumber(answer)
  351. if answer then
  352. if answer >= 1 and answer <= options_number then
  353. print("Selection: " .. options[answer])
  354. func(options[answer])
  355. else
  356. print("Error: Not in dropdown range")
  357. return nil
  358. end
  359. else
  360. print("Error: Must be a number")
  361. return nil
  362. end
  363. end
  364. --- Allows user to select from a set of options
  365. -- @function modpol.interactions.checkbox_query
  366. -- @param user Name of user (string)
  367. -- @param label Query for user before options (string)
  368. -- @param options Table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
  369. -- @param func Function to be called with param "input", made up of the corrected table in the same format as the param options
  370. function modpol.interactions.checkbox_query(
  371. user, label, options, func)
  372. -- set up options
  373. local options_display = ""
  374. local options_number = 0
  375. for i,v in ipairs(options) do
  376. local checked = false
  377. if v[2] then checked = true end
  378. if checked then
  379. checked = "x"
  380. else
  381. checked = " "
  382. end
  383. options_display = options_display..i..". ["..
  384. checked.."] "..v[1].."\n"
  385. options_number = options_number + 1
  386. end
  387. if options_number == 0 then
  388. print("Error: No options given for dropdown")
  389. return nil
  390. end
  391. options_display = options_display..
  392. "List comma-separated options to flip (e.g., 1,2,5):"
  393. -- begin displaying
  394. print(user .. ": " .. label)
  395. print(options_display)
  396. -- read input and produce output
  397. local answer = io.read()
  398. local answer_table = {}
  399. for match in (answer..","):gmatch("(.-)"..",") do
  400. table.insert(answer_table, tonumber(match))
  401. end
  402. local result_table = modpol.util.copy_table(options)
  403. for i,v in ipairs(answer_table) do
  404. if result_table[v] then
  405. -- flip the boolean on selected options
  406. result_table[v][2] = not result_table[v][2]
  407. end
  408. end
  409. func(result_table)
  410. end
  411. --- Output: Applies "func" to user input.
  412. -- Func input: user input (string: y/n)
  413. -- @function modpol.interactions.binary_poll_user
  414. -- @param user (string)
  415. -- @param question (string)
  416. -- @param func (function)
  417. function modpol.interactions.binary_poll_user(user, question, func)
  418. local query = "Poll for " .. user .. " (y/n): ".. question
  419. local answer
  420. repeat
  421. print(query)
  422. answer = io.read()
  423. until answer == "y" or answer == "n"
  424. if answer == "y" then
  425. modpol.interactions.message(user, "Response recorded")
  426. func("Yes")
  427. elseif answer == "n" then
  428. modpol.interactions.message(user, "Response recorded")
  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