Browse Source

Major refactoring (big thanks to OldCoder) enabling CLI and local storage and cleaner modpol/MT split

Nathan Schneider 3 years ago
parent
commit
c927b4d9fc
16 changed files with 1824 additions and 251 deletions
  1. 1 0
      .gitignore
  2. 44 10
      README.md
  3. 1 0
      depends.txt
  4. 105 240
      init.lua
  5. 25 0
      interactions.lua
  6. 2 1
      mod.conf
  7. 77 0
      modpol.lua
  8. 743 0
      ocutil.lua
  9. 192 0
      orgs.lua
  10. 21 0
      serpent/LICENSE.txt
  11. 293 0
      serpent/README.md
  12. BIN
      serpent/serpent-git.tar.bz2
  13. 148 0
      serpent/serpent.lua
  14. 117 0
      storage-local.lua
  15. 26 0
      storage-mod_storage.lua
  16. 29 0
      users.lua

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/data

+ 44 - 10
README.md

@@ -1,23 +1,57 @@
-# modpol: Modular Politics for Minetest
+# modpol: Modular Politics Prototype for Minetest
 
-This is a mod for [Minetest](https://minetest.net) that provides an API for diverse governance mechanisms. It seeks to implement the [Modular Politics](https://metagov.org/modpol) proposal.
+This is a mod for [Minetest](https://minetest.net) that enables diverse governance mechanisms. It seeks to implement the [Modular Politics](https://metagov.org/modpol) proposal. Ideally, in the future, it
+will be possible to use this framework to simulate governance in a
+number of platform contexts.
 
-This mod produces an API that can serve as a dependency for other mods that add specific governance functionalities.
+This mod produces an API that can serve as a dependency for other mods that add specific governance functionalities. 
+
+Currently modpol requires a Unix-style system. But it is intended to become more fully platform independent. Here, only the init.lua (and the optional storage-mod_storage.lua) files are Minetest-specific.
 
 For background information and project roadmap, see [the wiki](https://gitlab.com/medlabboulder/modpol/-/wikis/home).
 
-## Functioning commands
+## Command line
+
+To interact with the interpreter on Unix systems in CLI mode, install
+lua or luajit and execute the following command in this directory:
+
+```
+$ lua
+> dofile("modpol.lua")
+```
+
+## Minetest
 
-Most of these commands will later be buried under other commands that do more privilege checking. These are mainly for testing purposes.
+To use this in Minetest, simply install it as a Minetest mod. Minetest
+will load init.lua. See the source code for information about chat
+commands which can then be used.
 
-* `/neworg [orgname]` - Create a new org
+Most of these commands will later be buried under other commands that
+do more privilege checking. These are mainly for testing purposes.
+
+* `/addorg [orgname]` - Create a new org
 * `/listorgs` - Lists the orgs (and their members) currently in existence
-* `/rmorgs` - Reset orgs to just "instance" with all online players
 * `/listplayers` - Lists all the players currently in the game
 * `/joinorg [orgname]` - Adds the user to the specified org
-* `/listmembers [orgname]` - Lists the players currently in the specified org
 * `/pollself [question]` - Asks the player a yes/no/abstain question
 
----
 
-Initiated by [Nathan Schneider](https://nathanschneider.info) of the [Media Enterprise Design Lab](https://colorado.edu/lab/medlab) at the University of Colorado Boulder, as part of the [Metagovernance Project](https://metagov.org), in collaboration with the Minetest community—see the initial [forum post](https://forum.minetest.net/viewtopic.php?f=47&t=26037). Contributions welcome.
+## Storage
+
+By default, a  data  directory  named  "data" will  be created in  this directory. "/data" will contain a log file and serialized program data files.
+
+Another storage method may be chosen in modpol.lua. A StorageRef-based method for Minetest 5.* is included: storage-mod_storage.lua.
+
+
+## Authorship
+
+Initiated by [Nathan Schneider](https://nathanschneider.info) of the [Media Enterprise Design Lab](https://colorado.edu/lab/medlab) at the University of Colorado Boulder, as part of the [Metagovernance Project](https://metagov.org). Based on the paper "[Modular Politics: Toward a Governance Layer for Online Communities](https://metagov.org/modpol)."
+
+We'd love to have more contributors, particularly from the Minetest community! Please join the conversation in the [Issues](https://gitlab.com/medlabboulder/modpol/-/issues) or the [Minetest.net forum](https://forum.minetest.net/viewtopic.php?f=47&t=26037).
+
+Thanks to contributors: Robert Kiraly [[OldCoder](https://github.com/oldcoder/)] (ocutils.lua, storage-local.lua, project refactoring)
+
+## Licenses
+
+* [Project](LICENSE.mt): MIT-based Hippocratic License
+* [Lua Serpent Serializer](serpent/LICENSE.txt): MIT

+ 1 - 0
depends.txt

@@ -0,0 +1 @@
+default

+ 105 - 240
init.lua

@@ -1,176 +1,117 @@
---[[ INITIALIZING: basics ]]--
-
--- global API table
-modpol = {
-}
-
--- table for all active governance data
-modpol.orgs = {
-}
-
--- record of governance interactions
--- every state change should appear here
-modpol.ledger = {
-}
-
--- update from mod_storage
--- https://dev.minetest.net/StorageRef
-local mod_storage = minetest.get_mod_storage()
--- load orgs
-local stored_orgs = minetest.deserialize(mod_storage:get_string("orgs"))
-if (stored_orgs ~= nil) then
-   modpol.orgs = stored_orgs
-end
--- load orgs
-local stored_ledger = minetest.deserialize(mod_storage:get_string("ledger"))
-if (stored_ledger ~= nil) then
-   modpol.ledger = stored_ledger
-end
-
-
---[[ FUNCTIONS:basics ]]--
-
--- record(message, org)
--- writes all governance events to storage and ledger
-function modpol.record(message, org)
-   -- record to ledger
-   table.insert(modpol.ledger, message)
-   -- record to org_ledger
-   if (modpol.orgs[org] ~= nil) then
-      local org_ledg = modpol.orgs[org]["ledger"]
-      if (org_ledg == nil) then
-         modpol.orgs[org]["ledger"] = {message}
-      else
-         modpol.orgs[org]["ledger"] = table.insert(org_ledg,message)
+-- ===================================================================
+-- /init.lua
+-- Modular Politics (modpol) for Minetest
+-- TKTK Maybe make this just a quick ref file and locate MT files elsewhere?
+-- TKTK need to add player to orgs.instance with on_joinplayer
+
+-- ===================================================================
+-- Load modpol system
+
+dofile(minetest.get_modpath("modpol") .. "/modpol.lua")
+
+-- ===================================================================
+-- Modular Politics functions
+-- Overwriting default API functions with platform-specific ones
+-- ===================================================================
+
+-- ===================================================================
+-- Function: modpol.list_users(org)
+-- Overwrites function at /users.lua
+-- Params:
+-- if nil, lists instance members; if an org name, lists its members
+-- Output: a table with names of players currently in the game
+modpol.list_users = function(org)
+   local users = {}
+   if (org == nil) then -- no specified org; all players
+      for _,player in ipairs(minetest.get_connected_players()) do
+         local name = player:get_player_name()
+         table.insert(users,name)
+      end
+   else -- if an org is specified
+      if (modpol.orgs[org] ~= nil) then -- org exists
+         users = modpol.orgs[org]["members"]
       end
    end
-   -- record to storage
-   mod_storage:set_string("orgs", minetest.serialize(modpol.orgs))
-   mod_storage:set_string("ledger", minetest.serialize(modpol.ledger))
-end
-
---[[ FUNCTIONS:orgs ]]--
-
--- new_org()
--- create an org and add it to the list
-function modpol.new_org(orgName, orgMembers)
-   if (orgName == "") then -- blank orgName input
-      return "-!- Org needs a name"
-   end
-   if (modpol.orgs[orgName] == nil) then -- copy check
-      modpol.orgs[orgName] = {members = orgMembers}
-   else return "-!- Org already exists"
-   end
-   local message = "New org: " .. orgName ..
-      " (" .. table.concat(orgMembers, ", ") .. ")"
-   modpol.record(message, orgName)
-   return message
+   return members
 end
 
--- /neworg [name]
--- creates a new org with the current user as sole member
-minetest.register_chatcommand(
-   "neworg", {
-      privs = {},
-      func = function(user, param)
-         local result =
-            modpol.new_org(param,{user})
-         return true, result
-      end, 
-})
-
--- rename_org()
-function modpol.rename_org(oldName, newName)
-   -- TKTK
-   local message = "Org renamed: " .. oldName .. " > " .. newName
-   modpol.record(message, newName)
+-- ===================================================================
+-- Function: modpol.binary_poll_user(user, question)
+-- Overwrites function at /interactions.lua
+-- presents a yes/no/abstain poll to a user, returns answer
+modpol.binary_poll_user = function(user, question)
+   -- set up formspec
+   local text = "Poll: " .. question
+   local formspec = {
+      "formspec_version[4]",
+      "size[5,3]",
+      "label[0.375,0.5;", minetest.formspec_escape(text), "]",
+      "button[1,1.5;1,0.8;yes;Yes]",
+      "button[2,1.5;1,0.8;no;No]",    
+      "button[3,1.5;1,0.8;abstain;Abstain]"
+      --TKTK can we enable text wrapping?
+   }
+   local formspec_string = table.concat(formspec, "")
+   -- present to player
+   minetest.show_formspec(user, "modpol:binary_poll", formspec_string)
 end
 
+-- ===================================================================
+-- Minetest commands
+-- ===================================================================
 
--- /listorgs
--- lists the orgs currently in the game
-minetest.register_chatcommand(
-   "listorgs", {
-      privs = {},
-      func = function(user, param)
-         local orglist = ""
-         for key, value in pairs(modpol.orgs) do
-            -- first, set up member list
-            local membs = modpol.orgs[key]["members"]
-            if (membs == nil) then membs = ""
-            else membs = " (" .. table.concat(membs, ", ") .. ")"
-            end
-            -- now, assemble the list
-            if (orglist == "") -- first element only
-            then orglist = key .. membs
-            else orglist = orglist .. ", " .. key .. membs
-            end
-         end
-         return true, "Orgs: " .. orglist
-      end,
-})
+local chat_table                -- MT chat command definitions table
+local regchat                   -- Chat-command registration function
 
--- rm_orgs()
--- removes all orgs
-function modpol.rm_orgs()
-   modpol.orgs["instance"] = {members = modpol.list_members()}
-   local message = "Orgs purged"
-   modpol.record(message, nil)
-   return message
-end
+regchat = minetest.register_chatcommand
 
--- /rmorgs
-minetest.register_chatcommand(
-   "rmorgs", {
-      privs = {},
-      func = function(user, param)
-         modpol.orgs = {}
-         return true, modpol.rm_orgs()
-      end,
-})
+-- ===================================================================
+-- /addorg  /add_org
+-- This code defines a chat command which creates a new
+-- "org". Presently, the command makes the user the sole member of the
+-- "org".
 
---[[ FUNCTIONS:users ]]--
+chat_table = {
+   privs        = {} ,
+   func         = function (user, param)
+      local result = modpol.add_org (param, { user })
+      return true, result
+   end        
+}
+regchat ("addorg"  , chat_table)
+regchat ("add_org" , chat_table)
+
+-- ===================================================================
+-- /listorg  /listorgs  /list_org  /list_orgs
+-- In Minetest mode,  this code defines a chat command which lists the
+-- existing "orgs".
+-- The list shows one "org" per line in the following format:
+-- org_name (member, member, ...)
+
+chat_table = {
+    privs = {} ,
+    func  = function (user, param)
+        return true, "Orgs:\n" .. modpol.list_orgs()
+    end
+}
 
--- list_members(domain)
--- produces a table with names of players currently in the game
--- if empty, lists all players; if an org name, lists its members
-function modpol.list_members(domain)
-   local members = {}
-   if (domain == nil) then -- no specified domain; all players
-      for _,player in ipairs(minetest.get_connected_players()) do
-         local name = player:get_player_name()
-         table.insert(members,name)
-      end
-   else -- if an org is specified
-      if (modpol.orgs[domain] ~= nil) then -- org exists
-         members = modpol.orgs[domain]["members"]
-      end
-   end
-   return members
-end
+regchat ("listorg"   , chat_table)
+regchat ("listorgs"  , chat_table)
+regchat ("list_org"  , chat_table)
+regchat ("list_orgs" , chat_table)
 
+-- ===================================================================
 -- /listplayers
 minetest.register_chatcommand(
    "listplayers", {
       privs = {},
       func = function(user)
-         local result = table.concat(modpol.list_members(),", ")
+         local result = table.concat(modpol.list_users(),", ")
          return true, "All players: " .. result
       end,
 })
 
-function modpol.add_member(org, member)
-   if (modpol.orgs.org == nil) then
-      return "-!- No such org"
-   else
-      table.insert(modpol.orgs.org["members"], member)
-      local message = member .. " added to org " .. org
-      modpol.record(message, org)
-      return message
-   end
-
-end
-
+-- ===================================================================
 -- /joinorg
 minetest.register_chatcommand(
    "joinorg", {
@@ -182,62 +123,23 @@ minetest.register_chatcommand(
 })
 
 
-function modpol.remove_member(org, member)
--- remove from all child orgs also
-   local message = member .. " removed from org " .. org
-   modpol.record(message, org)
-   return message
-end
-
--- /listmembers [org]
--- lists the members of an org
+-- ===================================================================
+-- /pollself [question]
+-- asks the user a question specified in param
 minetest.register_chatcommand(
-   "listmembers", {
+   "pollself", {
       privs = {},
       func = function(user, param)
-         local orglist = modpol.list_members(param)
-         return true, param .. ": " .. table.concat(orglist,", ")
+         modpol.binary_poll_user(user, param)
+         return true, result
       end,
 })
 
+-- ===================================================================
+-- Minetest events
+-- ===================================================================
 
--- PERMISSIONS FUNCTIONS
-
-
--- PRIVILEGE FUNCTIONS
--- Minetest-specific
--- manages user privileges according to org membership
-
-function modpol.assign_privilege(org, privilege)
--- add privilege to all members of an org
-end
-
-
-function modpol.remove_privilege(org, privilege)
--- remove privilege from all members of an org, unless they have it from other orgs
-end
-
---[[ USER INTERACTIONS ]]--
-
--- modpol.binary_poll_user(user, question)
--- presents a yes/no/abstain poll to a user, returns answer
-function modpol.binary_poll_user(user, question)
-   -- set up formspec
-   local text = "Poll: " .. question
-   local formspec = {
-      "formspec_version[4]",
-      "size[5,3]",
-      "label[0.375,0.5;", minetest.formspec_escape(text), "]",
-      "button[1,1.5;1,0.8;yes;Yes]",
-      "button[2,1.5;1,0.8;no;No]",    
-      "button[3,1.5;1,0.8;abstain;Abstain]"
-      --TKTK can we enable text wrapping?
-   }
-   local formspec_string = table.concat(formspec, "")
-   -- present to player
-   minetest.show_formspec(user, "modpol:binary_poll", formspec_string)
-end
-
+-- ===================================================================
 -- Receiving fields
 minetest.register_on_player_receive_fields(function (player, formname, fields)
       -- modpol:poll
@@ -258,42 +160,5 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
       end
 end)
 
--- /pollself [question]
--- asks the user a question
-minetest.register_chatcommand(
-   "pollself", {
-      privs = {},
-      func = function(user, param)
-         modpol.binary_poll_user(user, param)
-         return true, result
-      end,
-})
-
---[[ TKTK need to enable more complex ineractions
-   - checkboxes, radio
-   - write-in
-]]--
-
--- MESSAGE FUNCTIONS
-
-function modpol.org_message(org, speaker, message)
-   -- If org doesn't exit, broadcast to all
-   -- If org doesn't exist, don't broadcast
-   -- use: minetest.chat_send_player("player1", "This is a chat message for player1")
-end
--- register at chat command for this
-
-
--- Add HUD interface that shows status: orgs, privileges https://rubenwardy.com/minetest_modding_book/en/players/hud.html
-
--- toggle it on and off
-
-
-
---[[ INITIALIZING: post-functions ]]--
-
--- create instance if not present
-if (modpol.orgs["instance"] == nil) then
-   modpol.new_org("instance", modpol.list_members())
-end
--- TKTK does this need to be on_joinplayer? still isn't adding the singleplayer
+-- ===================================================================
+-- End of file.

+ 25 - 0
interactions.lua

@@ -0,0 +1,25 @@
+-- ===================================================================
+-- /orgs.lua
+-- User interaction functions for Modular Politics
+-- Called by modpol.lua
+
+-- ===================================================================
+-- Function: modpol.binary_poll_user(user, question)
+-- Params: user (string), question (string)
+-- Output:
+-- presents a yes/no/abstain poll to a user, returns answer
+modpol.binary_poll_user = function(user, question)
+   local query = "Poll for " .. user .. " (y/n/a): ".. question
+   local answer
+   repeat
+      print(query)
+      answer= io.read()
+   until answer == "y" or answer == "n" or answer == "a"
+   if answer == "y" then
+      return "Yes"
+   elseif answer == "n" then
+      return "No"
+   else
+      return "Abstain"
+   end
+end

+ 2 - 1
mod.conf

@@ -1,7 +1,8 @@
 name = modpol
+depends = default
 title = Modular Politics
 author = ntnsndr <n@nathanschneider.info>
-description = A governance layer API for Minetest
+description = A governance layer framework in Lua
 license = Hippocratic License
 forum = TBA
 version = 0.1

+ 77 - 0
modpol.lua

@@ -0,0 +1,77 @@
+-- ===================================================================
+-- /modpol.lua
+-- Modular Politics (modpol) core
+
+-- ===================================================================
+-- Basic tables
+
+-- Main API table
+modpol = {
+}
+
+-- Table for all active governance data
+modpol.orgs = {
+}
+
+-- Record of every state change should appear here
+modpol.ledger = {
+}
+
+-- ===================================================================
+-- Locate framework top-level directory.
+
+-- This function is intended for use under Linux and/or UNIX only.  It
+-- returns  a relative or  absolute path  for the framework  top-level
+-- directory without a trailing slash.
+
+local get_script_dir = function()
+    local  str = debug.getinfo (2, "S").source:sub (2)
+           str = str:match ("(.*/)") or "."
+           str = str:gsub ("/$", "", 1)
+    return str
+end
+
+-- Absolute or relative path to script directory.
+local  topdir = get_script_dir()
+modpol.topdir = topdir
+print (topdir)
+
+-- ===================================================================
+-- Load dependencies
+
+-- OldCoder utilities
+dofile (topdir .. "/ocutil.lua")
+
+-- ===================================================================
+-- Persistent storage
+-- must implement modpol.load_storage() and modpol.store_data()
+
+-- Select a storage method
+-- Works with CLI:
+dofile (topdir .. "/storage-local.lua")
+-- Works with Minetest 5:
+-- dofile (topdir .. "/storage-mod_storage.lua")
+
+-- If available, load persistent storage into active tables
+modpol.load_storage()
+
+-- ===================================================================
+-- ModPol core features
+
+dofile (topdir .. "/users.lua")
+dofile (topdir .. "/orgs.lua")
+dofile (topdir .. "/interactions.lua")
+-- messaging functions
+
+-- ===================================================================
+-- Final checks
+
+-- create instance if not present
+if (modpol.orgs["instance"] == nil) then
+   modpol.add_org("instance", modpol.list_users())
+end
+
+ocutil.log ("modpol loaded")
+
+-- ===================================================================
+-- End of file.

+ 743 - 0
ocutil.lua

@@ -0,0 +1,743 @@
+-- ===================================================================
+-- Overview.
+
+-- This file is "top/ocutil.lua".
+
+-- This file  provides a  collection of  largely portable  Lua utility
+-- functions.  The collection is descended  from one assembled by Old-
+-- Coder (Robert Kiraly).
+
+-- ===================================================================
+
+ocutil = {}
+
+ocutil.log_console = true       -- Flag: Copy log to console
+ocutil.logdir      = nil        -- Absolute path for  log-file  direc-
+                                -- tory (or nil)
+
+-- ===================================================================
+
+-- Function: ocutil.fixnil
+-- Params:   string s
+-- Outputs:
+--
+-- If the string is nil   , returns "(nil)"
+-- if the string is empty , returns "(empty)"
+-- Otherwise, returns the string as it stands
+--
+-- This function can be used  to clarify -- and/or avoid crashes in --
+-- debug messages.
+
+-- ===================================================================
+
+ocutil.fixnil = function (s)
+    if     s == nil then s = "(nil)"
+    elseif s == ""  then s = "(empty)"
+    end
+    return s
+end
+
+-- ===================================================================
+
+-- Function: ocutil.setlogdir
+-- Params:   string path
+-- Outputs:
+--
+-- The input string should  be the absolute path for a writable direc-
+-- tory that already exists.  This function tells  "ocutil.log" to put
+-- log files in that directory.
+
+-- ===================================================================
+
+ocutil.setlogdir = function (path)
+    if path ~= nil and path ~= "" then
+        ocutil.logdir = path
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.setlogname
+-- Params:   string name
+-- Outputs:
+--
+-- The input string  should  be a filename  without a  path component.
+-- This function tells  "ocutil.log" to use the specified filename for
+-- the main log file.
+--
+-- "ocutil.setlogdir"  must be called  separately  to set the log-file
+-- directory.
+
+-- ===================================================================
+
+ocutil.setlogname = function (name)
+    if name ~= nil and name ~= "" then
+        ocutil.logname = name
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.log
+-- Params:   string s
+-- Outputs:
+--
+-- Logs the specified string.  Appends a  newline if not  already pre-
+-- sent.
+
+-- ===================================================================
+
+ocutil.log = function (s)
+    s = ocutil.fixnil (s)
+    s = s:gsub ("\n$", "", 1)   -- Remove trailing newline initially
+
+    if ocutil.log_console then
+        print (s)
+    end
+
+    if ocutil.logdir  ~= nil and
+       ocutil.logname ~= nil then
+        s = s .. "\n"           -- Add trailing newline
+
+        local logpath = ocutil.logdir .. "/" .. ocutil.logname
+        ocutil.file_append (logpath, s)
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.fatal_error
+-- Params:   string s
+-- Outputs:
+--
+-- Logs "Fatal error: " plus the specified string.  Appends a  newline
+-- if not already present. Terminates the program.
+
+-- ===================================================================
+
+ocutil.fatal_error = function (s)
+    ocutil.log ("Fatal Error: " .. s)
+    os.exit (1)
+    return nil                  -- Shouldn't be reached
+end
+
+-- ===================================================================
+
+-- Function: ocutil.panic
+-- Params:   string s
+-- Outputs:
+--
+-- Logs "Internal error: " plus the specified string.  Appends a  new-
+-- line if not already present. Terminates the program.
+
+-- ===================================================================
+
+ocutil.panic = function (s)
+    ocutil.log ("Internal Error: " .. s)
+    os.exit (1)
+    return nil                  -- Shouldn't be reached
+end
+
+-- ===================================================================
+
+-- Function: ocutil.str_empty
+-- Params:   string x
+-- Outputs:
+--
+-- Returns true if the string is nil or empty
+-- Returns false otherwise
+
+-- ===================================================================
+
+ocutil.str_empty = function (x)
+    if x == nil or x == "" then
+        return true
+    else
+        return false
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.str_nonempty
+-- Params:   string x
+-- Outputs:
+--
+-- Returns true if the string is nil or empty
+-- Returns false otherwise
+
+-- ===================================================================
+
+ocutil.str_nonempty = function (x)
+    if x == nil or x == "" then
+        return false
+    else
+        return true
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.table_empty
+-- Params:   table tab
+-- Outputs:
+--
+-- Returns true if the table is empty
+-- Returns false otherwise
+
+-- ===================================================================
+
+ocutil.table_empty = function (tab)
+    local next = next
+    if next (tab) == nil then return true end
+    return false
+end
+
+-- ===================================================================
+
+-- Function: ocutil.table_nonempty
+-- Params:   table tab
+-- Outputs:
+--
+-- Returns false if the table is empty
+-- Returns true otherwise
+
+-- ===================================================================
+
+ocutil.table_nonempty = function (tab)
+    if ocutil.table_empty (tab) then return false end
+    return true
+end
+
+-- ===================================================================
+
+-- Function: ocutil.string_contains
+-- Params:   strings a, b
+-- Outputs:
+--
+-- Returns true if the 1st string contains the 2nd one
+-- Returns false otherwise
+
+-- ===================================================================
+
+ocutil.str_contains = function (a, b)
+    if string.match (a, b) then
+        return true
+    else
+        return false
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.str_false
+-- Params:   string x
+-- Outputs:
+--
+-- This function checks the string for an  explicit false  (or equiva-
+-- lent)  setting (as  opposed to empty or nil). If such a  setting is
+-- found, true is returned. Otherwise, false is returned.
+
+-- ===================================================================
+
+ocutil.str_false = function (x)
+    if x == "false" or x == "no"  or
+       x == "off"   or x == "0"   or
+       x == false   or x == 0 then
+        return true
+    else
+        return false
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.str_true
+-- Params:   string x
+-- Outputs:
+--
+-- This function  checks the string for  an explicit true  (or equiva-
+-- lent) setting. If such a setting is found, true is returned. Other-
+-- wise, false is returned.
+
+-- ===================================================================
+
+ocutil.str_true  = function (x)
+    if x == "true" or x == "yes" or
+       x == "on"   or x == "1"   or
+       x == true   or x == 1 then
+        return true
+    else
+        return false
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.starts_with
+-- Params:   strings String, Start
+-- Outputs:
+--
+-- Returns true if the 1st string starts with the 2nd one
+-- Returns false otherwise
+
+-- ===================================================================
+
+ocutil.starts_with = function (String, Start)
+    if string.sub (String, 1, string.len (Start)) == Start then
+        return true
+    else
+        return false
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.not_starts_with
+-- Params:   strings String, Start
+-- Outputs:
+--
+-- Returns false if the 1st string starts with the 2nd one
+-- Returns true otherwise
+
+-- ===================================================================
+
+ocutil.not_starts_with = function (String, Start)
+    if string.sub (String, 1, string.len (Start)) == Start then
+        return false
+    else
+        return true
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.ends_with
+-- Params:   strings String, End
+-- Outputs:
+--
+-- Returns true if the 1st string ends with the 2nd one
+-- Returns false otherwise
+-- As a special case, returns true if the 2nd string is empty
+
+-- ===================================================================
+
+ocutil.ends_with = function (String, End)
+    return End == '' or string.sub (String,
+        -string.len (End)) == End
+end
+
+-- ===================================================================
+
+-- Function: ocutil.not_ends_with
+-- Params:   strings String, End
+-- Outputs:
+-- Returns false if the 1st string ends with the 2nd one
+-- Returns true otherwise
+-- As a special case, returns false if the 2nd string is empty
+
+-- ===================================================================
+
+ocutil.not_ends_with = function (String, End)
+    if ocutil.ends_with (String, End) then
+        return false
+    else
+        return true
+    end
+end
+
+-- ===================================================================
+
+-- Function: ocutil.firstch
+-- Params:   string str
+-- Outputs:
+-- Returns the 1st character of the string
+
+-- ===================================================================
+
+ocutil.firstch = function (str)
+    return string.sub (str, 1, 1)
+end
+
+-- ===================================================================
+
+-- Function: ocutil.first_to_upper
+-- Params:   string str
+-- Outputs:
+-- Returns the 1st character of the string in upper case
+
+-- ===================================================================
+
+ocutil.first_to_upper = function (str)
+    return (str:gsub ("^%l", string.upper))
+end
+
+-- ===================================================================
+
+-- Function: ocutil.all_first_to_upper
+-- Params:   string str, flag cvtspace
+-- Outputs:
+--
+-- Converts  the  1st character  of each word in  the string  to upper
+-- case and  returns the result.  Words are delimited by spaces or un-
+-- derscores.  If the flag is true,  also replaces  spaces with under-
+-- scores.
+
+-- ===================================================================
+
+ocutil.all_first_to_upper = function (str, cvtspace)
+
+    str = str:gsub ("^%l", string.upper)
+    str = str:gsub ("[_ ]%l",
+        function (a) return string.upper (a) end)
+
+    if ocutil.str_true (cvtspace) then
+        str = str:gsub ("_", " ")
+    end
+
+    return (str)
+end
+
+-- ===================================================================
+
+-- Function: ocutil.strtok
+-- Params:   strings source, delimitch
+-- Outputs:
+--
+-- Breaks  the source string up into  a list of words and  returns the
+-- list.
+--
+-- If "delimitch" is  omitted,  nil, or empty,  words are delimited by
+-- spaces. Otherwise, words are  delimited by any of the characters in
+-- "delimitch".
+
+-- ===================================================================
+
+ocutil.strtok = function (source, delimitch)
+    if delimitch == nil or
+       delimitch == ""  then delimitch = " " end
+
+    local parts   = {}
+    local pattern = '([^' .. delimitch .. ']+)'
+
+    string.gsub (source, pattern,
+        function (value)
+            value = value:gsub ("^ *", "")
+            value = value:gsub (" *$", "")
+            parts [#parts + 1] = value
+        end)
+
+    return parts
+end
+
+-- ===================================================================
+
+-- Function: ocutil.swap_key_value
+-- Params:   table itable
+-- Outputs:
+-- Turns  keys into values and  vice versa and  returns  the resulting
+-- table.
+
+-- ===================================================================
+
+ocutil.swap_key_value = function (itable)
+    if itable == nil then return nil end
+    local  otable = {}
+    for key, value in pairs (itable) do otable [value] = key end
+    return otable
+end
+
+-- ===================================================================
+
+-- Function: ocutil.ktable_to_vtable
+-- Params:   table ktable
+-- Outputs:
+--
+-- "ktable_to_vtable" is short for "convert key table to value table".
+--
+-- The input table is assumed to be an arbitrary key-based list of the
+-- general form:
+-- { dog="woof", apple="red", book="read" }
+--
+-- This function takes the keys and returns them as an integer-indexed
+-- array with the former keys stored now as values. The array is sort-
+-- ed based on the former keys. The original values in the input table
+-- are discarded. Sample output:
+--
+-- { "apple", "book", "dog", ... }
+
+-- Here's  a  complete  code fragment which  demonstrates operation of
+-- this function:
+
+-- local tabby = { dog="woof", apple="red", book="read" }
+--
+-- print ("\nInput table:")
+-- for ii, value in  pairs (tabby) do
+--     print (ii .. ") " .. tabby [ii])
+-- end
+--
+-- tabby = ocutil.ktable_to_vtable (tabby)
+--
+-- print ("\nOutput table:")
+-- for ii, value in ipairs (tabby) do
+--     print (ii .. ") " .. tabby [ii])
+-- end
+
+-- ===================================================================
+
+ocutil.ktable_to_vtable = function (ktable)
+    local vtable = {}
+    if ktable == nil then return vtable end
+
+    for key, _ in pairs (ktable) do
+        table.insert (vtable, key)
+    end
+
+    table.sort (vtable)
+    return vtable
+end
+
+-- ===================================================================
+
+-- Function: ocutil.vtable_to_ktable
+-- Params:   table vtable, scalar set_to
+-- Outputs:
+--
+-- "vtable_to_ktable" is short for "convert value table to key table".
+--
+-- The input table is assumed to be an integer-indexed array of scalar
+-- values.
+--
+-- This function creates a general table that holds the  scalar values
+-- stored  now as keys and returns  the new table.  The new table maps
+-- each of the keys to the value specified by "set_to".
+--
+-- If "set_to" is omitted or nil, it defaults to boolean true.
+
+-- Here's  a  complete  code fragment which  demonstrates operation of
+-- this function:
+--
+-- local tabby = { "apple", "book", "dog" }
+--
+-- print ("\nInput table:")
+-- for ii, value in ipairs (tabby) do
+--     print (ii .. ") " .. tabby [ii])
+-- end
+--
+-- tabby = ocutil.vtable_to_ktable (tabby, 42)
+--
+-- print ("\nOutput table:")
+-- for ii, value in  pairs (tabby) do
+--     print (ii .. ") " .. tabby [ii])
+-- end
+
+-- ===================================================================
+
+ocutil.vtable_to_ktable = function (vtable, set_to)
+
+    local ktable = {}
+    if vtable == nil then return ktable end
+    if set_to == nil then set_to = true end
+
+    for _, value in pairs (vtable) do
+        ktable [value] = set_to
+    end
+
+    return ktable
+end
+
+-- ===================================================================
+
+-- Function: ocutil.make_set
+-- Params:   array harry
+-- Outputs:
+--
+-- This function makes a set out of an array. Specifically, it returns
+-- a new table with the  array's values as keys.  The  new table  maps
+-- each key to boolean true.
+--
+-- Here's  a  complete  code fragment which  demonstrates operation of
+-- this function:
+--
+-- local color_set = ocutil.make_set { "red", "green", "blue" }
+-- if    color_set ["red"] ~= nil then print ("Supported color") end
+
+-- ===================================================================
+
+ocutil.make_set = function (list)
+    local set = {}
+    for _, l in ipairs (list) do set [l] = true end
+    return set
+end
+
+-- ===================================================================
+
+-- Function: ocutil.pos_to_str
+-- Params:   position-table pos
+-- Outputs:
+--
+-- This function takes a table of the following form as input:
+-- { x=25.0, y=-135.5, z=2750.0 }
+--
+-- It returns the x-y-z values, separated by commas, as a string.
+
+-- ===================================================================
+
+ocutil.pos_to_str = function (pos)
+    return pos.x .. "," .. pos.y .. "," .. pos.z
+end
+
+-- ===================================================================
+
+-- Function: ocutil.table_length
+-- Params:   table tabby
+-- Outputs:  Returns number of entries in table
+--
+-- Note: This function works for tables in general as well as for int-
+-- eger-indexed tables (i.e., arrays).
+
+-- ===================================================================
+
+ocutil.table_length = function (tabby)
+    local count = 0
+    for _ in pairs (tabby) do count = count+1 end
+    return count
+end
+
+-- ===================================================================
+
+-- Function: ocutil.clone_table
+-- Params:   table tabby
+-- Outputs:
+--
+-- This function returns  an independent clone of the input table.  It
+-- should work correctly for most cases,  but special cases  that  re-
+-- quire additional work may exist.
+--
+-- This function may also be called as "ocutil.table_clone".
+
+-- ===================================================================
+
+ocutil.clone_table = function (tabby)
+    if tabby == nil then return nil end
+    local copy = {}
+    for k, v in pairs (tabby) do
+        if type (v) == 'table' then v = ocutil.clone_table (v) end
+        copy [k] = v
+    end
+    return copy
+end
+
+ocutil.table_clone = ocutil.clone_table
+
+-- ===================================================================
+
+-- Function: ocutil.file_exists
+-- Params:   string path
+-- Outputs:
+--
+-- The  input string should be the relative or absolute pathname for a
+-- data file.  If the  file is read-accessible,  this function returns
+-- true. Otherwise, it returns false.
+
+-- ===================================================================
+
+ocutil.file_exists = function (path)
+    local file, err
+    file, err = io.open (path, "rb")
+    if err then return false end
+    file:close()
+    return true
+end
+
+-- ===================================================================
+
+-- Function: ocutil.file_missing
+-- Params:   string path
+-- Outputs:
+--
+-- The  input string should be the relative or absolute pathname for a
+-- data file.  If the  file is read-accessible,  this function returns
+-- false. Otherwise, it returns true.
+
+-- ===================================================================
+
+ocutil.file_missing = function (path)
+    if ocutil.file_exists (path) then return false end
+    return true
+end
+
+-- ===================================================================
+
+-- Function: ocutil.file_read
+-- Params:   string path
+-- Outputs:
+--
+-- The  input string should be the  absolute pathname for a data file.
+-- If the file is read-accessible,  this function returns the contents
+-- of the file. Otherwise, it returns nil.
+--
+-- Warning: There is presently no check for a maximum file size.
+
+-- ===================================================================
+
+ocutil.file_read = function (path)
+    local file, err
+    file, err = io.open (path, "rb")
+    if err then return nil end
+    local data = file:read ("*a")
+    file:close()
+    return data
+end
+
+-- ===================================================================
+
+-- Function: ocutil.file_write
+-- Params:   strings path, data
+-- Outputs:
+--
+-- The 1st string should be the absolute pathname for a data file. The
+-- file  doesn't need to  exist initially but the  directory that will
+-- contain it does need to exist.
+--
+-- This function  writes the  2nd string to the file.  Existing stored
+-- data is discarded.
+--
+-- This function returns true if successful and false otherwise.
+
+-- ===================================================================
+
+ocutil.file_write = function (path, data)
+    local file, err
+    file, err = io.open (path, "wb")
+    if err then return false end
+    io.output (file)
+    io.write (data)
+    io.close (file)
+    return true
+end
+
+-- ===================================================================
+
+-- Function: ocutil.file_append
+-- Params:   strings path, data
+-- Outputs:
+--
+-- This function is  identical to  "ocutil.file_write"  except for one
+-- difference:  It  appends to existing files as opposed to overwrites
+-- them.
+
+-- ===================================================================
+
+ocutil.file_append = function (path, data)
+    local file, err
+    file, err = io.open (path, "ab")
+    if err then return false end
+    io.output (file)
+    io.write (data)
+    io.close (file)
+    return true
+end
+
+-- ===================================================================
+-- End of file.

+ 192 - 0
orgs.lua

@@ -0,0 +1,192 @@
+-- ===================================================================
+-- /orgs.lua
+-- Org-related functions for Modular Politics
+-- Called by modpol.lua
+
+-- ===================================================================
+-- Function: modpol.record
+-- Params: strings msg, org
+-- Outputs:
+--
+-- "msg" specifies an event and/or status message.
+-- "org" specifies an "org" name.
+--
+-- This  function adds  the message to a  global ledger  and, if "org"
+-- specifies a valid "org", to an "org"-specific ledger. Both the mem-
+-- ory-resident and on-disk copies of the data structures used are up-
+-- dated.
+
+modpol.record = function (org_name, msg)
+                                -- Record in global ledger
+   table.insert (modpol.ledger, msg)
+                                -- Record in "org"-specific ledger
+   if modpol.orgs [org_name] ~= nil then
+        local org_ledger =  modpol.orgs [org_name]["ledger"]
+        if    org_ledger == nil then
+            modpol.orgs [org_name]["ledger"] = { msg }
+        else
+            modpol.orgs [org_name]["ledger"] =
+                table.insert (org_ledger, msg)
+        end
+    end
+
+    modpol.store_data()         -- Copy data to disk
+end
+
+-- ===================================================================
+-- Function: modpol.add_org
+-- Params:   string name, table members
+-- Output:
+--
+-- This function creates an "org".  It returns either an error message
+-- starting with "-!-" or a success message.
+--
+-- The string parameter specifies the "org" name.
+--
+-- The  specified table should  be an  integer-indexed array of member
+-- names.
+
+modpol.add_org = function (org_name, members)
+    local str
+
+    if ocutil.str_empty (org_name) then
+        return "Error: Org needs a name"
+    end
+
+    ocutil.log ("Adding org " .. org_name)
+    if modpol.orgs [org_name] ~= nil then
+        str = "Error: Org " .. org_name .. " already exists"
+        ocutil.log (str)
+        return str
+    end
+
+    modpol.orgs [org_name] = { members=members }
+    local msg = "New org: " .. org_name ..
+        " (" .. table.concat (members, ", ") .. ")"
+
+   modpol.record (org_name, msg)
+   return msg
+end
+
+-- ===================================================================
+-- Function: modpol.list_orgs
+-- Params:   None
+-- Output:
+-- This function returns a text-format list of "orgs".  The list shows
+-- one "org" per line in the following format:
+-- org_name (member, member, ...)
+-- If there are no "orgs", the output is an empty string.
+
+modpol.list_orgs  = function()
+    local outbuf = ""
+    local str
+    for org_name, org_data in pairs (modpol.orgs) do
+                                -- Process next "org"
+                                -- Build string version of member list
+        local memcat = org_data ["members"]
+        if  ocutil.str_empty (memcat) then
+              memcat = "(empty)"
+        else
+              memcat = "(" .. table.concat (memcat, ", ") .. ")"
+        end
+                                -- Build output string
+        outbuf = outbuf .. org_name .. " " .. memcat .. "\n"
+    end
+    return outbuf
+end
+
+-- ===================================================================
+-- Function: modpol.reset_orgs
+-- Params:   None
+-- Removes all orgs and recreates blank org "instance" with all
+-- current users
+-- returns confirmation message
+
+modpol.reset_orgs = function()
+   local users = list_users()
+   modpol.orgs = {}
+   local message = "Orgs purged"
+   modpol.record(nil, message)
+   modpol.add_org("instance", users)
+   return message
+end
+
+-- ===================================================================
+-- Function: modpol.add_member
+-- Params: org (string), member (string)
+-- Output:
+-- Adds the specified member to the specified org
+-- Returns confirmation or error message
+
+modpol.add_member = function(org, member)
+   if (modpol.orgs[org] == nil) then
+      return "Error: No such org"
+   else
+      table.insert(modpol.orgs[org]["members"], member)
+      local message = member .. " added to org " .. org
+      modpol.record(message, org)
+      return message
+   end
+end
+
+-- ===================================================================
+-- Function: modpol.is_member
+-- Params: org (string), member (string)
+-- Output: Boolean depending on membership or nil/error message
+
+modpol.is_member = function(org, member)
+   if (modpol.orgs[org] == nil) then
+      return nil, "Error: No such org"
+   else
+      local member_table = modpol.orgs[org]["members"]
+      if member_table then
+         for index, value in pairs(member_table) do
+            if value == member then
+               -- match found
+               return true
+            end
+         end
+         -- no match found
+         return false
+      end
+      return false      
+   end
+end
+
+-- ===================================================================
+-- Function: modpol.remove_member
+-- Params: org (string), member (string)
+-- Output:
+-- Removes the specified member from the specified org
+-- Returns confirmation or error message
+
+function modpol.remove_member(org, member)
+   local message = "Error: No such org"
+   if (modpol.orgs[org] == nil) then
+      return nil, message
+   else
+      local member_table = modpol.orgs[org]["members"]
+      if member_table then
+         for index, value in pairs(member_table) do
+            if value == member then
+               -- match found, set to nil
+               value = nil               
+               message = member .. " removed from org " .. org
+               return message
+            end
+         end
+      end
+      -- no match found (or org has no members)
+      message = member .. " not found in org " .. org
+      return nil, message
+   end
+end
+
+-- ===================================================================
+-- TKTK:
+-- + rename_org(old_name, new_name)
+-- + 
+
+
+-- ===================================================================
+-- End of file.

+ 21 - 0
serpent/LICENSE.txt

@@ -0,0 +1,21 @@
+Serpent source is released under the MIT License
+
+Copyright (c) 2012-2018 Paul Kulchenko (paul@kulchenko.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 293 - 0
serpent/README.md

@@ -0,0 +1,293 @@
+# Serpent
+
+Lua serializer and pretty printer.
+
+## Features
+
+* Human readable:
+    * Provides single-line and multi-line output.
+    * Nested tables are properly indented in the multi-line output.
+    * Numerical keys are listed first.
+    * Keys are (optionally) sorted alphanumerically.
+    * Array part skips keys (`{'a', 'b'}` instead of `{[1] = 'a', [2] = 'b'}`).
+    * `nil` values are included when expected (`{1, nil, 3}` instead of `{1, [3]=3}`).
+    * Keys use short notation (`{foo = 'foo'}` instead of `{['foo'] = 'foo'}`).
+    * Shared references and self-references are marked in the output.
+* Machine readable: provides reliable deserialization using `loadstring()`.
+* Supports deeply nested tables.
+* Supports tables with self-references.
+* Keeps shared tables and functions shared after de/serialization.
+* Supports function serialization using `string.dump()`.
+* Supports serialization of global functions.
+* Supports `__tostring` and `__serialize` metamethods.
+* Escapes new-line `\010` and end-of-file control `\026` characters in strings.
+* Configurable with options and custom formatters.
+
+## Usage
+
+```lua
+local serpent = require("serpent")
+local a = {1, nil, 3, x=1, ['true'] = 2, [not true]=3}
+a[a] = a -- self-reference with a table as key and value
+
+print(serpent.dump(a)) -- full serialization
+print(serpent.line(a)) -- single line, no self-ref section
+print(serpent.block(a)) -- multi-line indented, no self-ref section
+
+local fun, err = loadstring(serpent.dump(a))
+if err then error(err) end
+local copy = fun()
+
+-- or using serpent.load:
+local ok, copy = serpent.load(serpent.dump(a))
+print(ok and copy[3] == a[3])
+```
+
+## Functions
+
+Serpent provides three functions that are shortcuts to the same
+internal function, but set different options by default:
+
+* `dump(a[, {...}])` -- full serialization; sets `name`, `compact` and `sparse` options;
+* `line(a[, {...}])` -- single line pretty printing, no self-ref section; sets `sortkeys` and `comment` options;
+* `block(a[, {...}])` -- multi-line indented pretty printing, no self-ref section; sets `indent`, `sortkeys`, and `comment` options.
+
+Note that `line` and `block` functions return pretty-printed data structures and if you want to deserialize them, you need to add `return` before running them through `loadstring`.
+For example: `loadstring('return '..require('mobdebug').line("foo"))() == "foo"`.
+
+While you can use `loadstring` or `load` functions to load serialized fragments, Serpent also provides `load` function that adds safety checks and reports an error if there is any executable code in the fragment.
+
+* `ok, res = serpent.load(str[, {safe = true}])` -- loads serialized fragment; you need to pass `{safe = false}` as the second value if you want to turn safety checks off.
+
+Similar to `pcall` and `loadstring` calls, `load` returns status as the first value and the result or the error message as the second value.
+
+## Options
+
+* indent (string) -- indentation; triggers long multi-line output.
+* comment (true/false/maxlevel) -- provide stringified value in a comment (up to `maxlevel` of depth).
+* sortkeys (true/false/function) -- sort keys.
+* sparse (true/false) -- force sparse encoding (no nil filling based on `#t`).
+* compact (true/false) -- remove spaces.
+* fatal (true/False) -- raise fatal error on non-serilizable values.
+* nocode (true/False) -- disable bytecode serialization for easy comparison.
+* nohuge (true/False) -- disable checking numbers against undefined and huge values.
+* maxlevel (number) -- specify max level up to which to expand nested tables.
+* maxnum (number) -- specify max number of elements in a table.
+* maxlength (number) -- specify max length for all table elements.
+* metatostring (True/false) -- use `__tostring` metamethod when serializing tables (**v0.29**);
+set to `false` to disable and serialize the table as is, even when `__tostring` is present.
+* numformat (string; "%.17g") -- specify format for numeric values as shortest possible round-trippable double (**v0.30**).
+Use "%.16g" for better readability and "%.17g" (the default value) to preserve floating point precision.
+* valignore (table) -- allows to specify a list of values to ignore (as keys).
+* keyallow (table) -- allows to specify the list of keys to be serialized.
+Any keys not in this list are not included in final output (as keys).
+* keyignore (table) -- allows to specity the list of keys to ignore in serialization.
+* valtypeignore (table) -- allows to specify a list of value *types* to ignore (as keys).
+* custom (function) -- provide custom output for tables.
+* name (string) -- name; triggers full serialization with self-ref section.
+
+These options can be provided as a second parameter to Serpent functions.
+
+```lua
+block(a, {fatal = true})
+line(a, {nocode = true, valignore = {[arrayToIgnore] = true}})
+function todiff(a) return dump(a, {nocode = true, indent = ' '}) end
+```
+
+Serpent functions set these options to different default values:
+
+* `dump` sets `compact` and `sparse` to `true`;
+* `line` sets `sortkeys` and `comment` to `true`;
+* `block` sets `sortkeys` and `comment` to `true` and `indent` to `'  '`.
+
+## Metatables with __tostring and __serialize methods
+
+If a table or a userdata value has `__tostring` or `__serialize` method, the method will be used to serialize the value.
+If `__serialize` method is present, it will be called with the value as a parameter.
+if `__serialize` method is not present, but `__tostring` is, then `tostring` will be called with the value as a parameter.
+In both cases, the result will be serialized, so `__serialize` method can return a table, that will be serialize and replace the original value.
+
+## Sorting
+
+A custom sort function can be provided to sort the contents of tables. The function takes 2 parameters, the first being the table (a list) with the keys, the second the original table. It should modify the first table in-place, and return nothing.
+For example, the following call will apply a sort function identical to the standard sort, except that it will not distinguish between lower- and uppercase.
+
+```lua
+local mysort  = function(k, o) -- k=keys, o=original table
+  local maxn, to = 12, {number = 'a', string = 'b'}
+  local function padnum(d) return ("%0"..maxn.."d"):format(d) end
+  local sort = function(a,b)
+    -- this -vvvvvvvvvv- is needed to sort array keys first
+    return ((k[a] and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))):upper()
+         < ((k[b] and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum))):upper()
+  end
+  table.sort(k, sort)
+end
+
+local content = { some = 1, input = 2, To = 3, serialize = 4 }
+local result = require('serpent').block(content, {sortkeys = mysort})
+```
+
+## Formatters
+
+Serpent supports a way to provide a custom formatter that allows to fully
+customize the output. The formatter takes four values:
+
+* tag -- the name of the current element with '=' or an empty string in case of array index,
+* head -- an opening table bracket `{` and associated indentation and newline (if any),
+* body -- table elements concatenated into a string using commas and indentation/newlines (if any),
+* tail -- a closing table bracket `}` and associated indentation and newline (if any), and
+* level -- the current level.
+
+For example, the following call will apply
+`Foo{bar} notation to its output (used by Metalua to display ASTs):
+
+```lua
+print((require "serpent").block(ast, {comment = false, custom =
+  function(tag,head,body,tail)
+    local out = head..body..tail
+    if tag:find('^lineinfo') then
+      out = out:gsub("\n%s+", "") -- collapse lineinfo to one line
+    elseif tag == '' then
+      body = body:gsub('%s*lineinfo = [^\n]+', '')
+      local _,_,atag = body:find('tag = "(%w+)"%s*$')
+      if atag then
+        out = "`"..atag..head.. body:gsub('%s*tag = "%w+"%s*$', '')..tail
+        out = out:gsub("\n%s+", ""):gsub(",}","}")
+      else out = head..body..tail end
+    end
+    return tag..out
+  end}))
+```
+
+## Limitations
+
+* Doesn't handle userdata (except filehandles in `io.*` table).
+* Threads, function upvalues/environments, and metatables are not serialized.
+
+## Performance
+
+A simple performance test against `serialize.lua` from metalua, `pretty.write`
+from Penlight, and `tserialize.lua` from lua-nucleo is included in `t/bench.lua`.
+
+These are the results from one of the runs:
+
+* nucleo (1000): 0.256s
+* metalua (1000): 0.177s
+* serpent (1000): 0.22s
+* serpent (1000): 0.161s -- no comments, no string escapes, no math.huge check
+* penlight (1000): 0.132s
+
+Serpent does additional processing to escape `\010` and `\026` characters in
+strings (to address http://lua-users.org/lists/lua-l/2007-07/msg00362.html,
+which is already fixed in Lua 5.2) and to check all numbers for `math.huge`.
+The seconds number excludes this processing to put it on an equal footing
+with other modules that skip these checks (`nucleo` still checks for `math.huge`).
+
+## Author
+
+Paul Kulchenko (paul@kulchenko.com)
+
+## License
+
+See LICENSE file.
+
+## History
+
+### v0.30 (Sep 01 2017)
+  - Updated `pairs` to avoid using `__pairs` in Lua 5.2+.
+  - Added `metatostring` option to disable `__tostring` processing during serialization.
+  - Added `level` to the parameters of the custom function (closes #25).
+  - Added `maxlength` option to limit the space taken by table elements.
+  - Optimized serialization of functions when `nocode` option is specified.
+  - Added protection from `__serialize` check failing (pkulchenko/ZeroBraneStudio#732).
+  - Added `keyignore` option for the serializer.
+  - Added check for environments that may not have 'default' tables as tables.
+  - Added numeric format to preserve floating point precision (closes #17).
+  - Added using `debug.metatable` when available.
+  - Improved handling of failures in `__tostring` (pkulchenko/ZeroBraneStudio#446).
+
+### v0.28 (May 06 2015)
+  - Switched to a method proposed by @SoniEx2 to disallow function calls (#15).
+  - Added more `tostring` for Lua 5.3 support (pkulchenko/ZeroBraneStudio#401).
+  - Updated environment handling to localize the impact (#15).
+  - Added setting env to protect against assigning global functions (closes #15).
+  - Updated tests to work with Lua 5.3.
+  - Added explicit `tostring` for Lua 5.3 with `LUA_NOCVTN2S` set (pkulchenko/ZeroBraneStudio#401).
+  - Fixed crash when not all Lua standard libraries are loaded (thanks to Tommy Nguyen).
+  - Improved Lua 5.2 support for serialized functions.
+
+### v0.27 (Jan 11 2014)
+  - Fixed order of elements in the array part with `sortkeys=true` (fixes #13).
+  - Updated custom formatter documentation (closes #11).
+  - Added `load` function to deserialize; updated documentation (closes #9).
+
+### v0.26 (Nov 05 2013)
+  - Added `load` function that (safely) loads serialized/pretty-printed values.
+  - Updated documentation.
+
+### v0.25 (Sep 29 2013)
+  - Added `maxnum` option to limit the number of elements in tables.
+  - Optimized processing of tables with numeric indexes.
+
+### v0.24 (Jun 12 2013)
+  - Fixed an issue with missing numerical keys (fixes #8).
+  - Fixed an issue with luaffi that returns `getmetatable(ffi.C)` as `true`.
+
+### v0.23 (Mar 24 2013)
+  - Added support for `cdata` type in LuaJIT (thanks to [Evan](https://github.com/neomantra)).
+  - Added comment to indicate incomplete output.
+  - Added support for metatables with __serialize method.
+  - Added handling of metatables with __tostring method.
+  - Fixed an issue with having too many locals in self-reference section.
+  - Fixed emitting premature circular reference in self-reference section, which caused invalid serialization.
+  - Modified the sort function signature to also pass the original table, so not only keys are available when sorting, but also the values in the original table.
+
+### v0.22 (Jan 15 2013)
+  - Added ability to process __tostring results that may be non-string values.
+
+### v0.21 (Jan 08 2013)
+  - Added `keyallow` and `valtypeignore` options (thanks to Jess Telford).
+  - Renamed `ignore` to `valignore`.
+
+### v0.19 (Nov 16 2012)
+  - Fixed an issue with serializing shared functions as keys.
+  - Added serialization of metatables using __tostring (when present).
+
+### v0.18 (Sep 13 2012)
+  - Fixed an issue with serializing data structures with circular references that require emitting temporary variables.
+  - Fixed an issue with serializing keys pointing to shared references.
+  - Improved overall serialization logic to inline values when possible.
+
+### v0.17 (Sep 12 2012)
+  - Fixed an issue with serializing userdata that doesn't provide tostring().
+
+### v0.16 (Aug 28 2012)
+  - Removed confusing --[[err]] comment from serialized results.
+  - Added a short comment to serialized functions when the body is skipped.
+
+### v0.15 (Jun 17 2012)
+  - Added `ignore` option to allow ignoring table values.
+  - Added `comment=num` option to set the max level up to which add comments.
+  - Changed all comments (except math.huge) to be controlled by `comment` option.
+
+### v0.14 (Jun 13 2012)
+  - Fixed an issue with string keys with numeric values `['3']` getting mixed
+    with real numeric keys (only with `sortkeys` option set to `true`).
+  - Fixed an issue with negative and real value numeric keys being misplaced.
+
+### v0.13 (Jun 13 2012)
+  - Added `maxlevel` option.
+  - Fixed key sorting such that `true` and `'true'` are always sorted in
+    the same order (for a more stable output).
+  - Removed addresses from names of temporary variables (for stable output).
+
+### v0.12 (Jun 12 2012)
+  - Added options to configure serialization process.
+  - Added `goto` to the list of keywords for Lua 5.2.
+  - Changed interface to dump/line/block methods.
+  - Changed `math.huge` to 1/0 for better portability.
+  - Replaced \010 with \n for better readability.
+
+### v0.10 (Jun 03 2012)
+  - First public release.

BIN
serpent/serpent-git.tar.bz2


+ 148 - 0
serpent/serpent.lua

@@ -0,0 +1,148 @@
+local n, v = "serpent", "0.302" -- (C) 2012-18 Paul Kulchenko; MIT License
+local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
+local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
+local badtype = {thread = true, userdata = true, cdata = true}
+local getmetatable = debug and debug.getmetatable or getmetatable
+local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+
+local keyword, globals, G = {}, {}, (_G or _ENV)
+for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
+  'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
+  'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
+for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
+for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
+  for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end
+
+local function s(t, opts)
+  local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum
+  local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
+  local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
+  local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring
+  local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
+  local numformat = opts.numformat or "%.17g"
+  local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
+  local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
+    -- tostring(val) is needed because __tostring may return a non-string value
+    function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end
+  local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s))
+    or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
+    or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
+  local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end
+  local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
+    and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
+  local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
+    local n = name == nil and '' or name
+    local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
+    local safe = plain and n or '['..safestr(n)..']'
+    return (path or '')..(plain and path and '.' or '')..safe, safe end
+  local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding
+    local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
+    local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end
+    table.sort(k, function(a,b)
+      -- sort numeric keys first: k[key] is not nil for numerical keys
+      return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
+           < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
+  local function val2str(t, name, indent, insref, path, plainindex, level)
+    local ttype, level, mt = type(t), (level or 0), getmetatable(t)
+    local spath, sname = safename(path, name)
+    local tag = plainindex and
+      ((type(name) == "number") and '' or name..space..'='..space) or
+      (name ~= nil and sname..space..'='..space or '')
+    if seen[t] then -- already seen this element
+      sref[#sref+1] = spath..space..'='..space..seen[t]
+      return tag..'nil'..comment('ref', level) end
+    -- protect from those cases where __tostring may fail
+    if type(mt) == 'table' and metatostring ~= false then
+      local to, tr = pcall(function() return mt.__tostring(t) end)
+      local so, sr = pcall(function() return mt.__serialize(t) end)
+      if (to or so) then -- knows how to serialize itself
+        seen[t] = insref or spath
+        t = so and sr or tr
+        ttype = type(t)
+      end -- new value falls through to be serialized
+    end
+    if ttype == "table" then
+      if level >= maxl then return tag..'{}'..comment('maxlvl', level) end
+      seen[t] = insref or spath
+      if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
+      if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end
+      local maxn, o, out = math.min(#t, maxnum or #t), {}, {}
+      for key = 1, maxn do o[key] = key end
+      if not maxnum or #o < maxnum then
+        local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables
+        for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end
+      if maxnum and #o > maxnum then o[maxnum+1] = nil end
+      if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end
+      local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)
+      for n, key in ipairs(o) do
+        local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
+        if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
+        or opts.keyallow and not opts.keyallow[key]
+        or opts.keyignore and opts.keyignore[key]
+        or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
+        or sparse and value == nil then -- skipping nils; do nothing
+        elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
+          if not seen[key] and not globals[key] then
+            sref[#sref+1] = 'placeholder'
+            local sname = safename(iname, gensym(key)) -- iname is table for local variables
+            sref[#sref] = val2str(key,sname,indent,sname,iname,true) end
+          sref[#sref+1] = 'placeholder'
+          local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']'
+          sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path))
+        else
+          out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1)
+          if maxlen then
+            maxlen = maxlen - #out[#out]
+            if maxlen < 0 then break end
+          end
+        end
+      end
+      local prefix = string.rep(indent or '', level)
+      local head = indent and '{\n'..prefix..indent or '{'
+      local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
+      local tail = indent and "\n"..prefix..'}' or '}'
+      return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level)
+    elseif badtype[ttype] then
+      seen[t] = insref or spath
+      return tag..globerr(t, level)
+    elseif ttype == 'function' then
+      seen[t] = insref or spath
+      if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end
+      local ok, res = pcall(string.dump, t)
+      local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level)
+      return tag..(func or globerr(t, level))
+    else return tag..safestr(t) end -- handle all other types
+  end
+  local sepr = indent and "\n" or ";"..space
+  local body = val2str(t, name, indent) -- this call also populates sref
+  local tail = #sref>1 and table.concat(sref, sepr)..sepr or ''
+  local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''
+  return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"
+end
+
+local function deserialize(data, opts)
+  local env = (opts and opts.safe == false) and G
+    or setmetatable({}, {
+        __index = function(t,k) return t end,
+        __call = function(t,...) error("cannot call functions") end
+      })
+  local f, res = (loadstring or load)('return '..data, nil, nil, env)
+  if not f then f, res = (loadstring or load)(data, nil, nil, env) end
+  if not f then return f, res end
+  if setfenv then setfenv(f, env) end
+  return pcall(f)
+end
+
+local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
+
+if false then
+return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
+  load = deserialize,
+  dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
+  line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
+  block = function(a, opts) return s(a, merge({indent = '  ', sortkeys = true, comment = true}, opts)) end }
+end
+
+modpol.serpent.load  = deserialize
+modpol.serpent.dump  = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end
+modpol.serpent.line  = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end
+modpol.serpent.block = function(a, opts) return s(a, merge({indent = '  ', sortkeys = true, comment = true}, opts)) end

+ 117 - 0
storage-local.lua

@@ -0,0 +1,117 @@
+-- ===================================================================
+-- /storage-local.lua
+-- Persistent storage in /data using Serpent Serializer
+-- Unix systems only
+
+-- ===================================================================
+-- Set directories and filenames
+
+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)
+
+ocutil.setlogdir  (modpol.datadir)
+ocutil.setlogname ("modpol.log")
+
+-- ===================================================================
+-- Set up the Serpent Serializer functions.
+
+modpol.serpent = {}
+dofile (modpol.topdir .. "/serpent/serpent.lua")
+
+-- ===================================================================
+-- This function stores "ledger" data to disk.
+
+local store_ledger = function()
+    local ok =  ocutil.file_write (modpol.file_ledger,
+modpol.serpent.dump (modpol.ledger))
+
+    if    ok ~= true then
+        ocutil.fatal_error ("store_data: ledger")
+    end
+
+    local nn  = ocutil.table_length (modpol.ledger)
+    local str = "entries"
+    if nn == 1 then str = "entry" end
+    ocutil.log (nn .. " global ledger entries stored to disk")
+end
+
+-- ===================================================================
+-- This function stores "orgs" data to disk.
+
+local store_orgs = function()
+    local ok =  ocutil.file_write (modpol.file_orgs,
+modpol.serpent.dump (modpol.orgs))
+
+    if    ok ~= true then
+        ocutil.fatal_error ("store_data: orgs")
+    end
+
+    local nn  = ocutil.table_length (modpol.orgs)
+    local str = "entries"
+    if nn == 1 then str = "entry" end
+    ocutil.log (nn .. " orgs stored to disk")
+end
+
+-- ===================================================================
+-- This function stores data to disk.
+
+modpol.store_data = function()
+    store_ledger()
+    store_orgs()
+end
+
+-- ===================================================================
+-- This function loads "ledger" data from disk.
+
+local load_ledger = function()
+    local obj =  ocutil.file_read (modpol.file_ledger )
+    if    obj ~= nil then
+        local func, err = load (obj)
+        if err then
+            ocutil.fatal_error ("load_data: ledger"   )
+        end
+        modpol.ledger = func()
+
+        local nn  = ocutil.table_length (modpol.ledger)
+        local str = "entries"
+        if nn == 1 then str = "entry" end
+        ocutil.log (nn .. " global ledger entries loaded from disk")
+    else
+        ocutil.log ("No stored global ledger data found")
+    end
+end
+
+-- ===================================================================
+-- This function loads "orgs" data from disk.
+
+local load_orgs   = function()
+    local obj =  ocutil.file_read (modpol.file_orgs )
+    if    obj ~= nil then
+        local func, err = load (obj)
+        if err then
+            ocutil.fatal_error ("load_data: orgs"   )
+        end
+        modpol.orgs = func()
+
+        local nn  = ocutil.table_length (modpol.orgs)
+        local str = "entries"
+        if nn == 1 then str = "entry" end
+        ocutil.log (nn .. " orgs loaded from disk")
+    else
+        ocutil.log ("No stored orgs data found")
+    end
+end
+
+-- ===================================================================
+-- This function loads stored data from disk.
+
+modpol.load_storage = function()
+    load_ledger()
+    load_orgs()
+end
+
+-- ===================================================================
+-- End of file.

+ 26 - 0
storage-mod_storage.lua

@@ -0,0 +1,26 @@
+-- ===================================================================
+-- /storage-mod_storage.lua
+-- Persistent storage via Minetest's mod_storage method
+-- See https://dev.minetest.net/StorageRef
+
+-- Loads content of stored orgs and ledger from mod_storage
+modpol.load_storage = function()
+   local mod_storage = minetest.get_mod_storage()
+   -- load orgs
+   local stored_orgs = minetest.deserialize(mod_storage:get_string("orgs"))
+   if (stored_orgs ~= nil) then
+      modpol.orgs = stored_orgs
+   end
+   -- load orgs
+   local stored_ledger = minetest.deserialize(mod_storage:get_string("ledger"))
+   if (stored_ledger ~= nil) then
+      modpol.ledger = stored_ledger
+   end
+end
+
+-- Stores content of current orgs and ledger to mod_storage
+modpol.store_data = function()
+   -- write to storage
+   mod_storage:set_string("orgs", minetest.serialize(modpol.orgs))
+   mod_storage:set_string("ledger", minetest.serialize(modpol.ledger))
+end

+ 29 - 0
users.lua

@@ -0,0 +1,29 @@
+-- ===================================================================
+-- /users.lua
+-- User-related functions for Modular Politics
+-- Called by modpol.lua
+
+-- ===================================================================
+-- Function: modpol.list_users
+-- Params: org
+-- Outputs: Table of user names
+--
+-- This may be overwritten by the platform-specific interface
+
+modpol.list_users = function(org)
+   local users = {}
+   if (org == nil) then -- no specified org; all players
+      if modpol.orgs["instance"]
+         and modpol.orgs["instance"]["members"] then
+         -- if instance exists and has membership
+         users = modpol.orgs["instance"]["members"]
+      else
+         users = {}
+      end
+   else -- if an org is specified
+      if (modpol.orgs[org] ~= nil) then -- org exists
+         users = modpol.orgs[org]["members"]
+      end
+   end
+   return users
+end