Compare commits

..

5 Commits

36 changed files with 698 additions and 407 deletions

3
.gitignore vendored
View File

@ -1,2 +1 @@
*/data/*
!*/data/placeholder
*/data

View File

@ -1,12 +1,26 @@
# Modpol for Minetest
![](lib/empire-modpol.png)
Modpol, short for "modular politics," enables diverse governance processes on multi-user platforms. It offers a library with which users can choose, modify, and create modules that add specific governance functionalities.
This implementation is a mod for [Minetest](https://minetest.net), a free/open-source voxel game. It is designed to be adapted to other multi-user platforms that also employ Lua as an extension language.
**Learn more at [modpol.net](https://modpol.net).**
## How to use it
Modpol is built around groups called *orgs*. At the base is an org with all users in it, called `Root` by default. *Modules* enable people to do things within orgs, such as decide on membership, grant powers to the org, and much more. To get started in Minetest:
* Type the command `/mp`
* Select the org `Root`
* Choose one of its modules to make new orgs and craft their behavior
![](lib/module_list.png)
Modules can be nested in each other, so one module can rely on another module to accomplish a process. Users might use a module to unilaterally carry out actions in the game, or the module might require a group decision to do so. Users can also change the modules available to users of a given org. There are currently two ways of doing this:
* Admins can remove modules from the list of modules loaded in `modpol_core/api.lua` and `modpol_minetest/api.lua`. This will make those modules no longer available to any user.
* Players can change the modules available in a given org from within the program using the `Change modules` module. Removed modules can be re-added in any org by using `Change modules` again.
Modpol should give you the ability to do whatever kind of politics you want with your modules. If there is something you would like to do that is not available, [develop a module for it](https://gitlab.com/medlabboulder/modpol/-/wikis/Module-Writing-Guide) (or ask us for help!).
## Installation in Minetest
@ -14,7 +28,7 @@ To use this in Minetest, simply install it in your `mods/` or `worldmods/` folde
In the game, open the Modpol dashboard with the command `/mp`.
For testing purposes, players with the `privs` privilege (generally admins) can use the `/mptest` command, which resets all the orgs and opens a dashboard.\
For testing purposes, players with the `privs` privilege (generally admins) can use the `/mp` command, which resets all the orgs and opens a dashboard.\
## Standalone Version on the Command Line
@ -41,6 +55,23 @@ In the interpreter, for a list of global functions and tables, use `modpol.menu(
The persistent storage method may be chosen in `modpol.lua`. If available, Modpol uses Minetest's built-in StorageRef system for Minetest 5.*. If that is not available, or in CLI mode, data will be stored in a data directory at `modpol_core/data/`. This will contain a log file and serialized program data files.
## Design philosophy
Modpol seeks to implement a theoretical framework called "[modular politics](https://metagov.org/modpol)," which proposes these design goals:
* *Modularity*: Platform operators and community members should have the ability to construct systems by creating, importing, and arranging composable parts together as a coherent whole.
* *Expressiveness*: The governance layer should be able to implement as wide a range of processes as possible.
* *Portability*: Governance tools developed for one platform should be portable to another platform for reuse and adaptation.
* *Interoperability*: Governance systems operating on different platforms and protocols should have the ability to interact with each other, sharing data and influencing each other's processes.
Additionally, Modpol seeks to counteract the tendency for "[implicit feudalism](https://ntnsndr.in/ImplicitFeudalism)," according to which rigid, top-down power structures are the norm in online spaces. To this end, some design patterns include:
* *Groups, not roles*: While most platforms assign powers through particular permissions given to individuals, in Modpol, power lies in groups (which Modpol calls "orgs").
* *Consent, not oligarchy*: Rather than assuming that decisions will be made by a few power-holders, the software assumes that consent by all affected users is the norm.
* *Inheritance, not blank slates*: When a new group is formed, it inherits the patterns of what preceded it, rather than imagining that it is starting from scratch.
It is certainly possible to use Modpol to replicate practices of implicit feudalism, such as all-powerful admins, but doing so requires extra work to overcome these defaults.
## Documentation
Various guides are available at the [GitLab wiki](https://gitlab.com/medlabboulder/modpol/-/wikis/home).
@ -70,7 +101,7 @@ We are grateful for initial support for this project from a residency with [The
## Contributing
We'd love to welcome more contributors. Please join the conversation in the [Issues](https://gitlab.com/medlabboulder/modpol/-/issues), the \#modpol channel at the [Metagovernance Project](https://metagov.org) Slack, and the [Minetest.net forum](https://forum.minetest.net/viewtopic.php?f=47&t=26037).
We'd love to welcome more contributors. Please join the conversation in the [Issues](https://gitlab.com/medlabboulder/modpol/-/issues), our [Matrix.org channel](https://matrix.to/#/#minetest-modpol:matrix.org), and the [Minetest.net forum](https://forum.minetest.net/viewtopic.php?f=47&t=26037).
Learn more about the project and how to develop your own modules in [the wiki](https://gitlab.com/medlabboulder/modpol/-/wikis/home).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 KiB

View File

@ -12,22 +12,27 @@ dofile (localdir .. "/interactions/interactions.lua")
--modules
--TODO make this automatic and directory-based
dofile (localdir .. "/modules/add_child_org.lua")
dofile (localdir .. "/modules/change_modules.lua")
dofile (localdir .. "/modules/change_policy.lua")
dofile (localdir .. "/modules/consent.lua")
dofile (localdir .. "/modules/create_token.lua")
dofile (localdir .. "/modules/defer.lua")
dofile (localdir .. "/modules/display_policies.lua")
dofile (localdir .. "/modules/display_processes.lua")
dofile (localdir .. "/modules/join_org.lua")
dofile (localdir .. "/modules/leave_org.lua")
dofile (localdir .. "/modules/message_org.lua")
dofile (localdir .. "/modules/randomizer.lua")
dofile (localdir .. "/modules/remove_child_org.lua")
dofile (localdir .. "/modules/remove_member.lua")
dofile (localdir .. "/modules/remove_org.lua")
dofile (localdir .. "/modules/remove_process.lua")
dofile (localdir .. "/modules/rename_org.lua")
dofile (localdir .. "/modules/send_token.lua")
dofile (localdir .. "/modules/tokenomics.lua")
dofile (localdir .. "/storage/store-modules.lua")
modpol.load_modules(localdir .. "/modules")
-- dofile (localdir .. "/modules/add_child_org_consent.lua")
-- dofile (localdir .. "/modules/add_child_org.lua")
-- dofile (localdir .. "/modules/change_modules.lua")
-- dofile (localdir .. "/modules/change_policy.lua")
-- dofile (localdir .. "/modules/consent.lua")
-- dofile (localdir .. "/modules/create_token.lua")
-- dofile (localdir .. "/modules/defer_consent.lua")
-- dofile (localdir .. "/modules/display_policies.lua")
-- dofile (localdir .. "/modules/display_processes.lua")
-- dofile (localdir .. "/modules/join_org_consent.lua")
-- dofile (localdir .. "/modules/leave_org.lua")
-- dofile (localdir .. "/modules/message_org.lua")
-- dofile (localdir .. "/modules/randomizer.lua")
-- dofile (localdir .. "/modules/remove_child_consent.lua")
-- dofile (localdir .. "/modules/remove_member_consent.lua")
-- dofile (localdir .. "/modules/remove_org_consent.lua")
-- dofile (localdir .. "/modules/remove_org.lua")
-- dofile (localdir .. "/modules/remove_process.lua")
-- dofile (localdir .. "/modules/rename_org_consent.lua")
-- dofile (localdir .. "/modules/send_token.lua")
-- dofile (localdir .. "/modules/tokenomics.lua")

View File

@ -27,11 +27,7 @@ function modpol.interactions.get_policy_string(
this_policy =
tostring(this_org.policies[module_slug][k])
else
if not v then
this_policy = "none"
else
this_policy = tostring(v)
end
this_policy = tostring(v)
end
table.insert(output, k.." - "..this_policy)
end
@ -123,7 +119,7 @@ function modpol.interactions.dashboard(user)
print("Orgs and users reset")
modpol.interactions.dashboard(user)
elseif sel == "Q" or sel == "q" then
elseif sel == "Q" or "q" then
return
else
@ -152,9 +148,7 @@ function modpol.interactions.org_dashboard(user, org_string)
local children = {}
for k,v in ipairs(org.children) do
local this_child = modpol.orgs.get_org(v)
if this_child then
table.insert(children, this_child.name)
end
table.insert(children, this_child.name)
end
-- prepare modules menu
@ -171,13 +165,7 @@ function modpol.interactions.org_dashboard(user, org_string)
table.sort(modules)
-- list pending
local processes = {}
for i,v in ipairs(org.processes) do
if v ~= "deleted" then
processes[i] = org.processes[i]
end
end
local process_msg = #processes .. " total processes"
local process_msg = #org.processes .. " total processes"
if org.pending[user] then
process_msg = process_msg .. " (" ..
modpol.util.num_pairs(org.pending[user]) .. " pending)"
@ -219,7 +207,6 @@ function modpol.interactions.org_dashboard(user, org_string)
org.name, module.slug, "\n")..
"\n".."Proceed?",
function(input)
print("\n")
if input == "Yes" then
org:call_module(module_sel, user)
elseif input == "No" then
@ -231,17 +218,20 @@ function modpol.interactions.org_dashboard(user, org_string)
print("Error: Module not found.")
modpol.interactions.org_dashboard(user, org.id)
end
elseif sel == 'p' or sel == 'P' then -- Pending processes
elseif sel == 'p' or sel == 'P' then
local processes = {}
print("All processes: (* indicates pending)")
for i,v in ipairs(processes) do
local active = ''
if org.pending[user] then
if org.pending[user][v.id] then
active = '*'
for i,v in ipairs(org.processes) do
if v ~= "deleted" then
local active = ''
if org.pending[user] then
if org.pending[user][v.id] then
active = '*'
end
end
print("["..v.id.."] "..v.slug..active)
end
print("["..v.id.."] "..v.slug..active)
end
print()
print("Interact with which one (use [id] number)?")
@ -257,7 +247,6 @@ function modpol.interactions.org_dashboard(user, org_string)
if org.pending[user][process.id] then
org:interact(process.id, user)
end
modpol.interactions.org_dashboard(user, org.id)
end
elseif sel == 'b' or sel == 'B' then
modpol.interactions.dashboard(user)
@ -348,14 +337,14 @@ function modpol.interactions.display(user, title, message, completion)
modpol.interactions.message(
self.initiator, "Error: input not typed for display")
if completion then completion() else
modpol.interactions.dashboard(user)
modpol.intereactions.dashboard(user)
end
end
print(message)
print("\nEnter to continue")
io.read()
if completion then completion() else
modpol.interactions.dashboard(user)
modpol.intereactions.dashboard(user)
end
end
@ -478,8 +467,10 @@ function modpol.interactions.binary_poll_user(user, question, func)
answer = io.read()
until answer == "y" or answer == "n"
if answer == "y" then
modpol.interactions.message(user, "Response recorded")
func("Yes")
elseif answer == "n" then
modpol.interactions.message(user, "Response recorded")
func("No")
else
modpol.interactions.message(user, "Error: invalid response")

View File

@ -55,7 +55,7 @@ dofile (topdir .. "/util/misc.lua")
-- Select a storage method
-- -- preferably, declare this in the init.lua that calls modpol.lua This is the default.
-- Works with CLI:
modpol.storage_file_path = modpol.storage_file_path or topdir .. "/storage/storage-local.lua"
modpol.storage_file_path = modpol.storage_file_path or topdir .. "/storage/unix-storage.lua"
-- Works with Minetest 5:
--modpol.storage_file_path = modpol.storage_file_path or topdir .. "/storage/storage-mod_storage.lua")

View File

@ -1,4 +1,4 @@
--- Adds a child org
--- Adds a child org.
-- @module add_child_org
local add_child_org = {
@ -8,10 +8,9 @@ local add_child_org = {
}
add_child_org.data = {
child_name = "",
result = false
result = nil
}
add_child_org.config = {
approval_module = false
}
--- Initiate consent for new child org
@ -43,35 +42,23 @@ function add_child_org:initiate(result)
modpol.interactions.message(
self.initiator,
"Proposed child org: " .. input)
-- initiate consent process
self.data.result = result
self:call_module(
self.config.approval_module,
self.initiator,
{
prompt = "Create child org " ..
self.data.child_name .. "?",
votes_required = #self.org.members
},
function()
self:create_child_org()
end
)
-- create the org
self:create_child_org()
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
end
)
end
--- Create a new child org
--- Create a new child orgg
-- @function add_child_org:create_child_org
function add_child_org:create_child_org()
self.org:add_org(self.data.child_name, self.initiator)
modpol.interactions.message_org(
self.initiator,
self.org.name,
"Created child org in " .. self.org.name .. ": "
.. self.data.child_name)
"Created child org "
..self.data.child_name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end

View File

@ -0,0 +1,78 @@
--- Adds a child org.
-- Depends on `consent`
-- @module add_child_org_consent
local add_child_org_consent = {
name = "Add child org (consent)",
slug = "add_child_org_consent",
desc = "Create a child org in the current one with member consent"
}
add_child_org_consent.data = {
child_name = "",
result = nil
}
add_child_org_consent.config = {
}
--- Initiate consent for new child org
-- @function add_child_org_consent:initiate(result)
-- @param result Callback if this module is embedded in other modules
function add_child_org_consent:initiate(result)
modpol.interactions.text_query(
self.initiator,"Child org name: ",
function(input)
if input == "" then
modpol.interactions.message(
self.initiator,
"No name entered for child org")
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
self.org:delete_process(self.id)
return
elseif modpol.orgs.get_org(input) then
modpol.interactions.message(
self.initiator,
"Org name already in use")
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
self.org:delete_process(self.id)
if result then result() end
return
end
self.data.child_name = input
modpol.interactions.message(
self.initiator,
"Proposed child org: " .. input)
-- initiate consent process
self:call_module(
"consent",
self.initiator,
{
prompt = "Create child org " ..
self.data.child_name .. "?",
votes_required = #self.org.members
},
function()
self:create_child_org()
end
)
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
end
)
end
--- Create a new child orgg
-- @function add_child_org_consent:create_child_org
function add_child_org_consent:create_child_org()
self.org:add_org(self.data.child_name, self.initiator)
modpol.interactions.message_org(
self.initiator,
self.org.name,
"Consent reached: created child org "
..self.data.child_name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end
modpol.modules.add_child_org_consent = add_child_org_consent

View File

@ -1,10 +1,11 @@
--- change_modules
-- Depends on consent
-- @module change_modules
local change_modules = {
name = "Change modules",
name = "Change modules (consent)",
slug = "change_modules",
desc = "Add or remove modules from the org",
desc = "Add or remove modules from the org with member consent",
hide = false;
}
@ -16,7 +17,6 @@ change_modules.data = {
}
change_modules.config = {
approval_module = false
}
--- Initiate change in modules.
@ -68,7 +68,7 @@ function change_modules:initiate(result)
self.org:delete_process(self.id)
return
end
-- proceed with approval
-- proceed with consent
local query = "Accept module changes in org "..
self.org.name.."?"
self.data.summary = ""
@ -80,10 +80,11 @@ function change_modules:initiate(result)
table.concat(self.data.remove_modules,", ")
end
self:call_module(
self.config.approval_module,
"consent",
self.initiator,
{
prompt = query..self.data.summary
prompt = query..self.data.summary,
votes_required = #self.org.members
},
function()
self:implement_change()

View File

@ -9,11 +9,11 @@ local change_policy = {
}
change_policy.data = {
result = false
result = nil
}
change_policy.config = {
approval_module = false
approval = "none"
}
--- Change modules initiate
@ -23,7 +23,8 @@ function change_policy:initiate(result)
-- prepare module options
local available_modules = {}
for k,org_mod in pairs(modpol.modules) do
if self.org.policies[k] then
if not org_mod.hide and
self.org.policies[k] then
available_modules[org_mod.slug] = modpol.util.copy_table(org_mod)
end end
local modules_list = {}
@ -68,20 +69,12 @@ function change_policy:initiate(result)
self.initiator, "Choose a policy to change:",
policy_list,
function(policy_choice)
local readable_value =
tostring(modpol.modules[mod_choice][policy_choice])
if readable_value == "nil" then
readable_value = "none"
end
modpol.interactions.text_query(
self.initiator,
"Current " .. policy_choice .. " value: "
.. readable_value
.. "\nChange value to (or leave blank):",
"Current " .. policy_choice .. " value: " ..
tostring(modpol.modules[mod_choice][policy_choice])
.. "\nChange value to (be careful!):",
function(policy_input)
if policy_input == "" then
policy_input = false
end
self:approve_change(
mod_choice,
policy_choice,
@ -101,24 +94,17 @@ end
-- @param policy (string) policy slug
-- @param input (string) input content
function change_policy:approve_change(module_slug, policy, input)
self.org:call_module(
self.config.approval_module,
-- NEED TO ADD APPROVAL CODE for consent, etc.
modpol.interactions.message(
self.initiator,
{prompt = "Update " .. policy .. " policy on module " ..
module_slug .. " with: " .. input .. " ?"},
function()
modpol.interactions.message(
self.initiator,
"In ".. self.org.name .. " updating " .. policy ..
" policy on module " .. module_slug .. " with: " .. input)
self.org.policies[module_slug][policy] = input
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end,
self.id
)
"Updating " .. policy .. " policy on module " ..
module_slug .. " with: " .. input)
self.org.policies[module_slug][policy] = input
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
return
end
modpol.modules.change_policy = change_policy

View File

@ -2,10 +2,10 @@
-- @module consent
local consent = {
name = "Consent process",
slug = "consent",
desc = "A utility module other modules use for consent decisions",
hide = true
name = "Consent process",
slug = "consent",
desc = "A utility module other modules use for consent decisions",
hide = true
}
consent.data = {
@ -13,8 +13,8 @@ consent.data = {
}
consent.config = {
prompt = "Do you consent?",
votes_required = false
prompt = "Do you consent?",
votes_required = 1
}
--- Initiate consent
@ -22,22 +22,13 @@ consent.config = {
-- @param result Callback if this module is embedded in other modules
function consent:initiate(result)
self.data.result = result
-- if org is empty or no votes required, consent given
if self.org:get_member_count() == 0
or self.config.votes_required == 0 then
modpol.interactions.message_org(
self.initiator,
self.org.name,
"Consent reached: " .. self.config.prompt)
-- if org is empty, consent is given automatically
if self.org:get_member_count() == 0 then
if self.data.result then
self.data.result() end
self.org:delete_process(self.id)
else
-- otherwise, create poll
-- set default votes_required
if not self.config.votes_required then
self.config.votes_required = self.org:get_member_count()
end
for id, member in pairs(self.org.members) do
self.org:add_pending_action(self.id, member, "callback")
end
@ -63,10 +54,6 @@ function consent:callback(member)
"/"..self.config.votes_required..")"
)
if self.data.votes >= self.config.votes_required then
modpol.interactions.message_org(
self.initiator,
self.org.name,
"Consent reached: " .. self.config.prompt)
if self.data.result then
self.data.result() end
self.org:delete_process(self.id)

View File

@ -3,9 +3,9 @@
-- @module create_token
local create_token = {
name = "Create a token",
name = "Create a token (consent)",
slug = "create_token",
desc = "Creates an org token",
desc = "With org consent, creates an org token",
hide = false;
}
@ -13,8 +13,7 @@ create_token.data = {
}
create_token.config = {
token_name = "token",
approval_module = false
token_name = ""
}
--- Initiate function
@ -30,7 +29,7 @@ function create_token:initiate(result)
"tokenomics",
self.initiator,
{
approval_module = self.config.approval_module,
consent = true,
token_slug = self.config.token_name
},
function(input2)

View File

@ -1,30 +1,30 @@
--- Defer
-- @module defer
--- Defer consent
-- @module defer_consent
local defer = {
name = "Defer ",
slug = "defer",
desc = "Defers a decision to another org",
local defer_consent = {
name = "Defer consent",
slug = "defer_consent",
desc = "Defers consent on a decision to another org",
hide = true;
}
defer.data = {
defer_consent.data = {
}
--- Config for module
-- @field defer_org Name or ID of target org
-- @field approval_module module to use in target org
-- @field prompt String passed on to approval_module
defer.config = {
approval_module = "consent",
-- @field votes_required Threshold passed on to `consent`
-- @field prompt String passed on to `consent`
defer_consent.config = {
defer_org = "Root",
prompt = ""
votes_required = 1,
prompt = "Do you consent?"
}
--- Initiate function
-- @param result Callback if this module is embedded in other modules
-- @function defer:initiate
function defer:initiate(result)
-- @function defer_consent:initiate
function defer_consent:initiate(result)
local defer_org = modpol.orgs.get_org(self.config.defer_org)
if not defer_org then
modpol.interactions.message(
@ -32,21 +32,18 @@ function defer:initiate(result)
self.org:delete_process(self.id)
else
defer_org:call_module(
self.config.approval_module,
self.initiator,
"consent", self.initiator,
{
votes_required = self.config.votes_required,
prompt = self.config.prompt
},
function()
if result then result() end
end)
modpol.interactions.message(
self.initiator, "Defer: action sent to " .. defer_org.name)
end
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
end
--- (Required) Add to module table
modpol.modules.defer = defer
modpol.modules.defer_consent = defer_consent

View File

@ -29,15 +29,11 @@ function display_policies:initiate(result)
v2 = self.org.policies[k][k2]
end
local v2_string = ""
if not v2 then
v2_string = "none"
elseif type(v2) == "string" then
if type(v2) == "string" then
v2_string = v2
elseif type(v2) == "table"
or type(v2) == "number" then
v2_string = tostring(v2)
elseif type(v2) == "boolean" then
v2_string = tostring(v2)
else
v2_string = "Could not render"
end
@ -57,10 +53,10 @@ function display_policies:initiate(result)
"Policies in org "..self.org.name,
output,
function()
self.org:delete_process(self.id)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
end
)
end

View File

@ -19,7 +19,7 @@ display_processes.config = {
-- @param result Callback if this module is embedded in other modules
function display_processes:initiate(result)
local display_table = {}
for k,v in ipairs(self.org.processes) do
for k,v in pairs(self.org.processes) do
if v ~= "deleted" then
local input = v.id..": "..v.slug
table.insert(display_table, input)

View File

@ -0,0 +1,3 @@
change_modules-dropdown.lua
join_org.lua
template.lua

View File

@ -1,55 +1,44 @@
--- Join org
-- Adds initiator to an org
--- Adds a user to org
-- @module join_org
local join_org = {
name = "Join this org",
slug = "join_org",
desc = "Allows initiator to join this org"
join_org = {}
join_org.setup = {
name = "Join Org",
slug = "join_org",
desc = "If consent process is passed, initiator joins this org."
}
join_org.data = {
result = nil
}
join_org.config = {
approval_module = false
}
--- Initiate join org with consent
-- @function join_org:initiate
--- Adds the user to the org
-- @function join_org.initiate
-- @param result Callback if this module is embedded in other modules
function join_org:initiate(result)
if self.org:has_member(self.initiator) then
modpol.interactions.message(
self.initiator,
"You are already a member of this org")
if result then result() end
self.org:delete_process(self.id)
else
self.data.result = result
self:call_module(
self.config.approval_module,
self.initiator,
{
prompt = "Allow " .. self.initiator .. " to join?"
},
function ()
self:complete()
function join_org.initiate(result)
modpol.interactions.binary_poll_user(
initiator,
"Would you like to join " .. org.name,
function (resp)
if resp == "Yes" then
self.org:add_member(self.initiator)
end
end
)
end
end
--- Adds member to org, notifies org, and deletes process
-- @function join_org:complete
function join_org:complete()
self.org:add_member(self.initiator)
modpol.interactions.message_org(
self.initiator,self.org.name,
self.initiator .. " joined org " .. self.org.name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
for i, member in ipairs(self.org.members) do
self.org:add_pending_action(
member,
function ()
modpol.interactions.binary_poll_user(
member,
"Let " .. initiator .. " join " .. org.name .. "?",
function (resp)
end
)
end
)
end
if result then result() end
end
modpol.modules.join_org = join_org

View File

@ -0,0 +1,57 @@
--- Join org (consent).
-- A simple module that calls a consent process on an org to add a member.
-- Depends on the Consent module.
-- @module join_org_consent
local join_org_consent = {
name = "Join this org (consent)",
slug = "join_org_consent",
desc = "Adds member with consent of all members"
}
join_org_consent.data = {
result = nil
}
join_org_consent.config = {
}
--- Initiate join org with consent
-- @function join_org_consent:initiate
-- @param result Callback if this module is embedded in other modules
function join_org_consent:initiate(result)
if self.org:has_member(self.initiator) then
modpol.interactions.message(
self.initiator,
"You are already a member of this org")
if result then result() end
self.org:delete_process(self.id)
else
self.data.result = result
self:call_module(
"consent",
self.initiator,
{
prompt = "Allow " .. self.initiator .. " to join?",
votes_required = #self.org.members
},
function ()
self:complete()
end
)
end
end
--- Adds member to org, notifies org, and deletes process
-- @function join_org_consent:complete
function join_org_consent:complete()
self.org:add_member(self.initiator)
modpol.interactions.message_org(
self.initiator,self.org.name,
"Consent reached: " .. self.initiator ..
" joined org " .. self.org.name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end
modpol.modules.join_org_consent = join_org_consent

View File

@ -1,26 +1,26 @@
--- Remove child org
-- A simple module that calls a process on an org to remove its child.
-- @module remove_child_org
--- Remove child (consent).
-- A simple module that calls a consent process on an org to remove its child.
-- Depends on the Consent module.
-- @module remove_child_consent
local remove_child_org = {
name = "Remove child org",
slug = "remove_child_org",
desc = "Removes a child org."
local remove_child_consent = {
name = "Remove child (consent)",
slug = "remove_child_consent",
desc = "Removes a child org if all members of this org consent."
}
remove_child_org.data = {
remove_child_consent.data = {
result = nil,
child_to_remove = nil
}
remove_child_org.config = {
approval_module = false
remove_child_consent.config = {
}
--- Removes a child org with consent
-- @function remove_child_org:initiate
-- @function remove_child_consent:initiate
-- @param result Callback if this module is embedded in other modules
function remove_child_org:initiate(result)
function remove_child_consent:initiate(result)
local children = {}
for i,v in ipairs(self.org.children) do
local child = modpol.orgs.get_org(v)
@ -43,10 +43,11 @@ function remove_child_org:initiate(result)
function(input)
self.data.child_to_remove = modpol.orgs.get_org(input)
self:call_module(
self.config.approval_module,
"consent",
self.initiator,
{
prompt = "Remove child org "..input.."?"
prompt = "Remove child org "..input.."?",
votes_required = #self.org.members
},
function()
self:complete()
@ -58,18 +59,19 @@ function remove_child_org:initiate(result)
end
--- Complete the remove process
-- @function remove_child_org:complete
function remove_child_org:complete()
-- @function remove_child_consent:complete
function remove_child_consent:complete()
modpol.interactions.message_org(
self.initiator, self.data.child_to_remove.id,
"Removing this org: " .. self.data.child_to_remove.name)
"Removing org " .. self.data.child_to_remove.name ..
" by parent org consent")
modpol.interactions.message_org(
self.initiator, self.org.id,
"Removing child org of " .. self.org.name .. ": " ..
"Consent reached: removing org " ..
self.data.child_to_remove.name)
self.data.child_to_remove:delete()
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end
modpol.modules.remove_child_org = remove_child_org
modpol.modules.remove_child_consent = remove_child_consent

View File

@ -1,32 +1,29 @@
--- Removes member from org
-- @module remove_member
--- Calls consent to remove member from org
-- @module remove_member_consent
local remove_member = {
name = "Remove a member",
slug = "remove_member",
desc = "Removes org member"
local remove_member_consent = {
name = "Remove a member (consent)",
slug = "remove_member_consent",
desc = "Removes org member with consent of other members"
}
remove_member.data = {
remove_member_consent.data = {
member_to_remove = "",
result = nil
}
remove_member.config = {
approval_module = false
remove_member_consent.config = {
}
--- Removes given member from org
-- @function remove_member:initiate
-- @function remove_member_consent:initiate
-- @param result Callback if this module is embedded in other modules
function remove_member:initiate(result)
function remove_member_consent:initiate(result)
-- Abort if in root org
if self.org == modpol.instance then
modpol.interactions.message(
self.initiator,
"Members cannot be removed from the root org")
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
if result then result() end
self.org:delete_process(self.id)
else -- proceed if not root
@ -39,11 +36,12 @@ function remove_member:initiate(result)
function(input)
self.data.member_to_remove = input
self:call_module(
self.config.approval_module,
"consent",
self.initiator,
{
prompt = "Remove "..input..
" from org "..self.org.name.."?"
" from org "..self.org.name.."?",
votes_required = #self.org.members - 1
},
function()
self:complete()
@ -55,11 +53,11 @@ function remove_member:initiate(result)
end
--- Complete after consent
-- @function remove_member:complete
function remove_member:complete()
-- @function remove_member_consent:complete
function remove_member_consent:complete()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Removing "..
"Consent reached: removing "..
self.data.member_to_remove..
" from org "..self.org.name)
self.org:remove_member(self.data.member_to_remove)
@ -67,4 +65,4 @@ function remove_member:complete()
if self.data.result then self.data.result() end
end
modpol.modules.remove_member = remove_member
modpol.modules.remove_member_consent = remove_member_consent

View File

@ -1,57 +1,32 @@
--- Remove org
-- Removes the current org
--- A simple module that removes an org.
-- @module remove_org
local remove_org = {
remove_org = {
name = "Remove this org",
slug = "remove_org",
desc = "Removes this org."
desc = "Eliminates the org and all child orgs."
}
remove_org.data = {
result = nil
}
remove_org.config = {}
remove_org.data = {}
remove_org.config = {
approval_module = false
}
--- Remove org if all members consent
--- Removes org
-- @function remove_org:initiate
-- @param result Callback if this module is embedded in other modules
function remove_org:initiate(result)
if self.org == modpol.instance then
modpol.interactions.message(
self.initiator,
"Cannot remove root org")
if result then result() end
self.org:delete_process(self.id)
modpol.interactions.message(
self.initiator,
"Cannot remove the root org")
else
self.data.result = result
self:call_module(
self.config.approval_module,
self.initiator,
{
prompt = "Remove org " .. self.org.name .. "?"
},
function()
self:complete()
end
)
modpol.interactions.org_dashboard(
self.initiator, modpol.orgs.get_org(self.org.parent).name)
modpol.interactions.message_org(
self.initiator,self.org.id,
"Removing org: "..self.org.name)
self.org:delete()
modpol.interactions.dashboard(self.initiator)
-- call result function
end
end
--- Complete after approval
-- @function remove_org:complete
function remove_org:complete()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Removing org " .. self.org.name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
self.org:delete()
if result then result() end
end
modpol.modules.remove_org = remove_org

View File

@ -0,0 +1,58 @@
--- Remove org (consent)
-- A simple module that calls a consent process on an org to remove it.
-- Depends on the Consent module.
-- @module remove_org_consent
local remove_org_consent = {
name = "Remove this org (consent)",
slug = "remove_org_consent",
desc = "Removes an org if all members consent."
}
remove_org_consent.data = {
result = nil
}
remove_org_consent.config = {
}
--- Remove org if all members consent
-- @function remove_org_consent:initiate
-- @param result Callback if this module is embedded in other modules
function remove_org_consent:initiate(result)
if self.org == modpol.instance then
modpol.interactions.message(
self.initiator,
"Cannot remove root org")
if result then result() end
self.org:delete_process(self.id)
else
self.data.result = result
self:call_module(
"consent",
self.initiator,
{
prompt = "Remove org " .. self.org.name .. "?",
votes_required = #self.org.members
},
function()
self:complete()
end
)
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
end
end
--- Complete after consent
-- @function remove_org_consent:complete
function remove_org_consent:complete()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Consent reached: removing org " .. self.org.name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
self.org:delete()
end
modpol.modules.remove_org_consent = remove_org_consent

View File

@ -4,7 +4,7 @@
local remove_process = {
name = "Remove process",
slug = "remove_process",
desc = "User can remove own processes, approval required for those of others",
desc = "User can remove own processes, consent required for those of others",
hide = false;
}
@ -12,7 +12,6 @@ remove_process.data = {
}
remove_process.config = {
approval_module = "consent"
}
--- Initiate function
@ -72,12 +71,11 @@ function remove_process:initiate(result)
self.org:delete_process(self.id)
else
self:call_module(
self.config.approval_module,
"consent",
self.initiator,
{
prompt =
"Approve removal of process "
..process_choice.."?"
prompt = "Approve removal of process "..process_choice.."?",
votes_required = #self.org.members
},
function(input)
modpol.interactions.message_org(

View File

@ -1,26 +1,26 @@
--- Rename org
-- Calls a process on an org to rename it.
-- @module rename_org
--- Rename org (consent)
-- A simple module that calls a consent process on an org to rename it.
-- Depends on the Consent module.
-- @module rename_org_consent
local rename_org = {
name = "Rename this org",
slug = "rename_org",
desc = "Renames an org."
local rename_org_consent = {
name = "Rename this org (consent)",
slug = "rename_org_consent",
desc = "Renames an org if all members consent."
}
rename_org.data = {
rename_org_consent.data = {
result = nil,
new_name = nil
}
rename_org.config = {
approval_module = false
rename_org_consent.config = {
}
--- Renames the org after consent is reached
-- @function rename_org:initiate
-- @function rename_org_consent:initiate
-- @param result Callback if this module is embedded in other modules
function rename_org:initiate(result)
function rename_org_consent:initiate(result)
modpol.interactions.text_query(
self.initiator,"New org name: ",
function(input)
@ -50,11 +50,12 @@ function rename_org:initiate(result)
self.org.name .. " to " .. input)
-- initiate consent process
self:call_module(
self.config.approval_module,
"consent",
self.initiator,
{
prompt = "Change name of org " ..
self.org.name .. " to " .. input .. "?"
self.org.name .. " to " .. input .. "?",
votes_required = #self.org.members
},
function()
self:complete()
@ -66,9 +67,9 @@ function rename_org:initiate(result)
)
end
--- Changes the name of the org
-- @funciton rename_org
function rename_org:complete()
--- Changes the name of the org after consent is reached
-- @funciton rename_org_consent
function rename_org_consent:complete()
modpol.interactions.message_org(
self.initiator,
self.org.name,
@ -79,4 +80,4 @@ function rename_org:complete()
self.org:delete_process(self.id)
end
modpol.modules.rename_org = rename_org
modpol.modules.rename_org_consent = rename_org_consent

View File

@ -16,7 +16,7 @@ send_token.data = {
}
send_token.config = {
token_name = nil -- hidden configuration
token_name = ""
}
--- initiate function
@ -29,7 +29,7 @@ function send_token:initiate(result)
table.insert(token_list, k)
end
end
if #token_list == 0 then
if token_list == {} then
modpol.interactions.message(
self.initiator,
"No tokens in org")

View File

@ -26,12 +26,9 @@ module_template.data = {
-- When calling a module from within another module,
-- variables not defined in config will be ignored.
-- Default values set in config can be overridden
-- @field approval_module names a module that must pass before approval; defaults to false
-- @field field_1 ex: votes_required, default = 5
-- @field field_2 ex: voting_type, default = "majority"
module_template.config = {
approval_module = false -- visible but empty
hidden_config = nil -- not visible to users unless used
field_1 = 5
field_2 = "majority"
}

View File

@ -1,4 +1,5 @@
--- Tokenomics.
-- Depends on consent
-- @module tokenomics
local tokenomics = {
@ -13,13 +14,14 @@ tokenomics.data = {
}
--- Config for module
-- @field consent Require consent to create?
-- @field token_variables the data that goes into the token
-- @field token_slug A no-spaces slug for the token
-- @field initial_treasury Quantity in org treasury
-- @field negative_spend Boolean: can users spend negative tokens? (for mutual credit)
-- @field balances Table of user balances
tokenomics.config = {
approval_module = false,
consent = false,
token_slug = "token",
token_variables = {
treasury = 0,
@ -41,25 +43,29 @@ function tokenomics:initiate(result)
self.initiator, "Token slug taken, aborting")
self.org:delete_process(self.id)
else
self:call_module(
self.config.approval_module,
self.initiator,
{
prompt = "Create token " ..
self.config.token_slug .. "?"
},
function()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Creating token " ..
self.config.token_slug)
self:create_token()
end
)
if self.config.consent then
self:call_module(
"consent",
self.initiator,
{
prompt = "Create token "..
self.config.token_slug.."?",
votes_required = #self.org.members
},
function()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Consent reached: creating token "..
self.config.token_slug)
self:create_token()
end
)
else
self:create_token()
end
end
end
--- Create token
-- @function tokenomics:create_token
function tokenomics:create_token()

View File

@ -25,24 +25,19 @@ end
--- Return org when given its id or name
-- @function modpol.orgs.get_org
-- @param arg string for name of org or id of org, or nil if no org
-- @param arg string for name of org or id of org
-- @return org specified by id or name
function modpol.orgs.get_org(arg)
local output = nil
if type(arg) == 'string' then
for id, org in ipairs(modpol.orgs.array) do
if org ~= "deleted" then
if type(arg) == 'string' then
for id, org in ipairs(modpol.orgs.array) do
if org.name == arg then
output = org
return org
end
end
end
elseif type(arg) == 'number' then
if modpol.orgs.array[arg] ~= "deleted" then
output = modpol.orgs.array[arg]
end
end
return output
end
elseif type(arg) == 'number' then
return modpol.orgs.array[arg]
end
return nil
end
--- Return a table list of all org names
@ -85,7 +80,7 @@ function modpol.orgs.reset()
modpol.util.copy_table(modpol.instance.members)
for id, org in ipairs(modpol.orgs.array) do
if id > 1 then
modpol.orgs.array[id] = "deleted"
modpol.orgs.array[id] = "removed"
end
end
@ -219,18 +214,7 @@ function modpol.orgs:delete()
modpol.ocutil.log('Error in ' .. self.name .. ':delete -> cannot delete instance')
return false
end
-- remove from parent's list
local parent = modpol.orgs.get_org(self.id)
if parent then
for k,v in ipairs(parent.children) do
if v == self.id then
v = "deleted"
end
end
end
-- remove children
if #self.children > 0 then
for i, child_id in pairs(self.children) do
local child = modpol.orgs.get_org(child_id)
@ -239,8 +223,7 @@ function modpol.orgs:delete()
end
end
modpol.orgs.array[self.id] = 'deleted'
modpol.orgs.count = modpol.orgs.count - 1
modpol.orgs.array[self.id] = 'removed'
modpol.ocutil.log('Deleted org ' .. self.name .. ': ' .. self.id)
self:record('Deleted ' .. self.name .. ' and all child orgs', 'del_org')

View File

@ -3,28 +3,19 @@
--- Call modules
-- @function modpol.orgs.call_module
-- @param module_slug Same as module name (or false to run result)
-- @param module_slug Same as module name
-- @param intiator Initiator for module
-- @param config Config for module
-- @param result
function modpol.orgs:call_module(module_slug, initiator, config, result, parent_id)
-- first, if no slug, just run result
-- may not be necessary if we use false as default approval_module
if not module_slug then
if result() then
result()
end
return
-- if module doesn't exist, abort
elseif not modpol.modules[module_slug] then
modpol.ocutil.log('Error in ' .. self.name .. ':call_module -> module "' .. tostring(module_slug) .. '" not found')
return
end
if not modpol.modules[module_slug] then
modpol.ocutil.log('Error in ' .. self.name .. ':call_module -> module "' .. module_slug .. '" not found')
return
end
local index = #self.processes + 1
local module = modpol.modules[module_slug]
local index = #self.processes + 1
local module = modpol.modules[module_slug]
-- first applies any relevant org policies
-- then overrides with the config values given on input
@ -73,8 +64,6 @@ function modpol.orgs:call_module(module_slug, initiator, config, result, parent_
return index
end
--- Get the root process of the given id
-- @function modpol.orgs.get_root_process
-- @param id
@ -87,7 +76,7 @@ function modpol.orgs:get_root_process(id)
return process
end
--- Delete the process given id, return to dashboard
--- Delete the process given id
-- @function modpol.orgs.delete_process
-- @param id
function modpol.orgs:delete_process(id)
@ -108,7 +97,6 @@ function modpol.orgs:delete_process(id)
end
end
--- Delete process tree by id
-- @function modpol.orgs:delete_process_tree
-- @param id Id of process tree
@ -134,7 +122,7 @@ end
-- @param user
function modpol.orgs:remove_pending_action(process_id, user)
if self.pending[user] then
self.pending[user][process_id] = "deleted"
self.pending[user][process_id] = nil
end
end
@ -143,7 +131,7 @@ end
-- @param process_id
function modpol.orgs:wipe_pending_actions(process_id)
for user in pairs(self.pending) do
self.pending[user][process_id] = "deleted"
self.pending[user][process_id] = nil
end
end
@ -165,7 +153,7 @@ function modpol.orgs:has_pending_actions(user)
end
end
--- Create user interaction with given process
--- Interact a user with given process
-- @function modpol.orgs:interact
-- @param process_id
-- @param user

View File

@ -11,6 +11,8 @@ modpol.file_ledger = modpol.datadir .. "/ledger.dat"
modpol.file_orgs = modpol.datadir .. "/orgs.dat"
modpol.file_old_ledgers = modpol.datadir .. "/old_ledgers.dat"
os.execute ("mkdir -p " .. modpol.datadir)
modpol.ocutil.setlogdir (modpol.datadir)
modpol.ocutil.setlogname ("modpol.log")
@ -99,7 +101,7 @@ local load_orgs = function()
-- setmetatable(org, modpol.orgs)
-- end
local nn = modpol.orgs.count
local nn = modpol.ocutil.table_length (modpol.orgs.array)
local str = "entries"
if nn == 1 then str = "entry" end
modpol.ocutil.log (nn .. " orgs loaded from disk")

View File

@ -0,0 +1,23 @@
modpol.load_modules = function(path)
dofile (path .. "/add_child_org_consent.lua")
dofile (path .. "/add_child_org.lua")
dofile (path .. "/change_modules.lua")
dofile (path .. "/change_policy.lua")
dofile (path .. "/consent.lua")
dofile (path .. "/create_token.lua")
dofile (path .. "/defer_consent.lua")
dofile (path .. "/display_policies.lua")
dofile (path .. "/display_processes.lua")
dofile (path .. "/join_org_consent.lua")
dofile (path .. "/leave_org.lua")
dofile (path .. "/message_org.lua")
dofile (path .. "/randomizer.lua")
dofile (path .. "/remove_child_consent.lua")
dofile (path .. "/remove_member_consent.lua")
dofile (path .. "/remove_org_consent.lua")
dofile (path .. "/remove_org.lua")
dofile (path .. "/remove_process.lua")
dofile (path .. "/rename_org_consent.lua")
dofile (path .. "/send_token.lua")
dofile (path .. "/tokenomics.lua")
end

View File

@ -0,0 +1,59 @@
local lfs
-- checks if lua file system is installed
local using_lfs = pcall(function() lfs = require "lfs" end)
-- switches to legacy module loading if lfs is not available
if using_lfs then
-- loads file names to ignore into a table
function fetch_ignores(module_path)
local ignore_list = {}
-- checks if ignore.txt exists
local f_test = io.open(module_path .. "/ignore.txt", "r")
if not f_test then return {} end
-- puts each line of ignore.txt into the table
local f = io.lines(module_path .. "/ignore.txt")
for line in f do
table.insert(ignore_list, line)
end
return ignore_list
end
-- checks if a string is in a list
function check_list(ignore_list, name)
for i, v in ipairs(ignore_list) do
if v == name then
return true
end
end
return false
end
modpol.load_modules = function(module_path)
local loaded = 0
local ignored = 0
local ignores = fetch_ignores(module_path)
for file in lfs.dir(module_path) do
if file == "." or file == ".." then
-- ignoring current and parent directory
else
-- only looks for .lua files
if string.sub(file, -4, -1) == ".lua" then
-- doesn't load files in the ignore.txt
if check_list(ignores, file) then
ignored = ignored + 1
else
dofile(module_path .. "/" .. file)
loaded = loaded + 1
end
end
end
end
print(loaded .. " modules loaded (" .. ignored .. " ignored)")
end
else
dofile (modpol.topdir .. "/storage/store-modules-legacy.lua")
end

View File

@ -0,0 +1,109 @@
-- ===================================================================
-- /unix-storage.lua
-- Persistent storage in /data using Serpent Serializer
-- Unix systems only
modpol.datadir = modpol.topdir .. "/data"
modpol.file_ledger = modpol.datadir .. "/ledger.dat"
modpol.file_orgs = modpol.datadir .. "/orgs.dat"
os.execute ("mkdir -p " .. modpol.datadir)
modpol.ocutil.setlogdir (modpol.datadir)
modpol.ocutil.setlogname ("modpol.log")
modpol.serpent = {}
dofile (modpol.topdir .. "/util/serpent/serpent.lua")
-- ===================================================================
local write_file = function(path, data)
local file, err
file, err = io.open(path, "w")
if err then print(err) os.exit(1) end
file:write(data)
file:close()
end
local read_file = function(path)
local file, err
file, err = io.open(path, "r")
if err then return nil end
local data = file:read()
file:close()
return data
end
-- ===================================================================
local store_ledger = function(verbose)
local serialized_ledger = modpol.serpent.dump(modpol.ledger)
write_file(modpol.file_ledger, serialized_ledger)
local count = 0
for _ in pairs(modpol.ledger) do count = count + 1 end
if verbose then modpol.ocutil.log(count .. " ledger entries stored to disk")
end
end
local store_orgs = function(verbose)
local serialized_orgs = modpol.serpent.dump(modpol.orgs)
write_file(modpol.file_orgs, serialized_orgs)
local count = 0
for _ in pairs(modpol.orgs.array) do count = count + 1 end
if verbose then modpol.ocutil.log(count .. " orgs stored to disk")
end
end
modpol.store_data = function(verbose)
store_ledger(verbose)
store_orgs(verbose)
end
-- ===================================================================
local load_ledger = function()
local obj = read_file(modpol.file_ledger)
if obj ~= nil then
local func, err = load(obj)
if err then
modpol.ocutil.log("Error loading ledger data")
os.exit(1)
end
modpol.ledger = func()
local count = 0
for _ in pairs(modpol.ledger) do count = count + 1 end
modpol.ocutil.log(count .. " ledger entries loaded from disk")
else
modpol.ocutil.log("No stored ledger data found")
end
end
local load_orgs = function()
local obj = read_file(modpol.file_orgs)
if obj ~= nil then
local func, err = load(obj)
if err then
modpol.ocutil.log("Error loading org data")
os.exit(1)
end
modpol.orgs = func()
local count = 0
for _ in pairs(modpol.orgs.array) do count = count + 1 end
modpol.ocutil.log(count .. " orgs loaded from disk")
else
modpol.ocutil.log("No stored orgs found")
end
end
modpol.load_storage = function()
load_ledger()
load_orgs()
end

View File

@ -32,13 +32,3 @@ function modpol.util.num_pairs(t)
end
return i
end
function modpol.util.lazy_table_length(tbl, lazy_val)
local count = 0
for k, v in ipairs(tbl) do
if v ~= lazy_val then
count = count + 1
end
end
return count
end

View File

@ -137,15 +137,11 @@ function modpol.interactions.org_dashboard(user, org_string)
-- prepare children menu
local children = {}
if org.children then
for k,v in ipairs(org.children) do
local this_child = modpol.orgs.get_org(v)
if this_child then
table.insert(children, this_child.name)
end
end
table.sort(children)
for k,v in ipairs(org.children) do
local this_child = modpol.orgs.get_org(v)
table.insert(children, this_child.name)
end
table.sort(children)
-- prepare modules menu
local modules = {}