interactions.lua 15 KB

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