interactions.lua 15 KB

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