Initial commit
authorBrian Flowers <git-admn@bsflowers.net>
Wed, 18 Jan 2023 03:14:14 +0000 (22:14 -0500)
committerBrian Flowers <git-admn@bsflowers.net>
Wed, 18 Jan 2023 03:14:14 +0000 (22:14 -0500)
index.html [new file with mode: 0644]
mastowot.css [new file with mode: 0644]
mastowot.html [new file with mode: 0644]
mastowot.js [new file with mode: 0644]

diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..f914fb4
--- /dev/null
@@ -0,0 +1,1043 @@
+<html>
+  <head>
+    <title>MastoWoT?</title>
+    <style>
+      body {
+        background-color: black;
+        padding: 0px;
+        margin:  0px;
+        color:   #CAA;
+        font-family: "Audiowide", "Sans";
+      }
+
+      h1 {
+        width: 100%;
+        text-style: italic;
+        text-align: center;
+      }
+
+      h2 {
+        display: inline-block;
+        margin:  .3em .5em .5em .5em;
+        vertical-align: middle;
+      }
+
+      .panelbutton {
+        display: inline-block;
+      }
+
+      #settingsPanel #sourceHost {
+        display: inline-block;
+        width: 100%;
+      }
+
+      #hostsPanel {
+        display:   inline-block;
+        width:     calc( 70% - 10px );
+        min-width: 30em;
+        height:    calc(100% - 3em);
+        /*background-color: #600;
+        border: 3px solid #A00;*/
+        margin: 0px;
+        vertical-align: top;
+      }
+
+/*      #hostsTrusted, #hostsUntrusted, #hostsKnown, #manPanel {*/
+        .subpanel, #manPanel {
+        padding: 0em 1em;
+        border:  3px solid #C00;
+        border-radius:    1em;
+        background-color: #600;
+        margin-bottom: 1em;
+      }
+
+      #settingsPanel {
+        display:   inline-block;
+        width:     calc( 30% - 10px - 2em);
+        min-width: 10em;
+        height:    calc(100% - 2em);
+        background-color: #400;
+        border: 3px solid #C00;
+        margin: 0px;
+        border-radius: 1em;
+        padding: 1em;
+        vertical-align: top;
+      }
+
+      #settingsPanel a {
+        display: block;
+        width: 100%;
+        text-align: center;
+        margin-bottom: 1em;
+        font-weight: bold;
+        color: #C00;
+      }
+
+      #settingsPanel button {
+        display: block;
+        width: 100%;
+      }
+
+      #settingsPanel div {
+        border: 1px solid black;
+        border-radius: 1em;
+        padding: 0em 0em 1em 0em;
+        margin-top: 1em;
+      }
+
+      #settingsPanel h2 {
+        width: 100%;
+      }
+
+      #settingsPanel label {
+        margin-left: 10%;
+        display: inline-block;
+        width: 40%;
+      }
+
+      #settingsPanel input {
+        display: inline-block;
+        width: 40%;
+      }
+
+      #shade {
+        position: fixed;
+        width: 100%;
+        height: 100%;
+        background-color: black;
+        opacity: 0.8;
+        z-index: 100;
+        left: 0px;
+        top: 0px;
+      }
+
+      #shade-text {
+        position: fixed;
+        width: 40%;
+        height: 40%;
+        left: 30%;
+        top: 30%;
+        z-index: 150;
+      }
+
+      #shade-text-button {
+        position: fixed;
+        width: 40%;
+        height: 5%;
+        left: 30%;
+        top: 70%;
+        z-index: 150;
+        font-size: 3vh;
+      }
+
+      .subpanel .list {
+        display: none;
+      }
+
+      .subpanel .list .instanceElem:nth-child(2n+3) {
+        background-color: #C11;
+      }
+
+      .subpanel .list div {
+        display: block;
+        position: relative;
+        border-bottom: 1px solid black;
+      }
+
+      .subpanel .list div h3 {
+        display: inline-block;
+        width: calc( 50% - 20em );
+        margin: 0em;
+        padding: 0em;
+        overflow: scroll;
+      }
+
+      .subpanel .list div div {
+        display: inline-block;
+        position: relative;
+        width: 20em;
+        vertical-align: middle;
+      }
+
+      .subpanel .list div .comments {
+        display: inline-block;
+        margin: none;
+        width: 48%;
+        border: none;
+      }
+
+      .subpanel .list div div input[type=text] {
+        display: inline-block;
+        width: 3em;
+        vertical-align: middle;
+      }
+
+      .subpanel .list div div input[type=range] {
+        display: inline-block;
+        width: 17em;
+        vertical-align: middle;
+      }
+
+      .subpanel .list div div h3 {
+        width: 100%;
+      }
+
+      .subpanel .list div:first-child h3 {
+        font-weight: bold;
+        text-decoration: underline;
+      }
+    </style>
+    <script>
+      // Initialize the site by loading in a list of known hosts
+      var instances = [];
+      var subLists = [];
+
+      function init() {
+        var buttons = document.getElementsByClassName("panelButton");
+        for(var i = 0; i < buttons.length; i++) {
+          buttons[i].onclick = openPanel;
+        }
+      }
+
+      function synchronize() {
+        var sourceInstance = "https://" + document.getElementById("sourceHost").value; //mastodon.slightlycyberpunk.com";
+        fetch(sourceInstance + "/api/v1/instance/peers")
+          .then(response => response.json())
+          .then(peers => {
+            console.log(peers);
+//            var instances = [];
+            for(var i = 0; i < peers.length; i++) {
+//              var instance = directory[i].acct.replace( directory[i].username + "@", "" );
+              var instance = peers[i];
+
+              if( instances[instance] == null ) {
+                instances[instance] = new Object();
+                instances[instance].domain = instance;
+                instances[instance].reasons = [];
+              }
+            }
+//            console.log(instances);
+            resetHosts();
+          });
+
+        fetch(sourceInstance + "/api/v1/instance/domain_blocks")
+          .then(response => response.json())
+          .then(blocklist => {
+            console.log(blocklist);
+
+            for(var i = 0; i < blocklist.length; i++) {
+              instance = blocklist[i].domain;
+              if(instance.indexOf("*") != -1) {
+                continue;
+              }
+              if(instances[instance] == null) {
+                instances[instance] = new Object();
+                instances[instance].domain = instance;
+                instances[instance].reasons = [];
+              }
+
+              if( blocklist[i].comment == null ) {
+                blocklist[i].comment = "[ No comments provided ]"
+              }
+              if( blocklist[i].severity == "silence" ) {
+                instances[instance].trust = -1 * document.getElementById("threshold-suspend").value;
+                instances[inst].reasons[instance] = "SILENCED: "+blocklist[i].comment;
+              } else {
+                instances[instance].trust = -1 * document.getElementById("threshold-limit");
+                instances[inst].reasons[instance] = "LIMITED: "+blocklist[i].comment;
+              }
+//              instances
+            }
+        });
+
+//        document.getElementById("processButton").onclick = process;        
+      }
+
+      async function process() {
+        document.getElementById("processButton").disabled = true;
+        var total = 0;
+        for(instance in instances) {
+          if(instances[instance].trust > 0) {
+            try {
+              await fetch("https://"+instance+"/api/v1/instance/domain_blocks")
+                .then(response => response.json())
+                .then(blocklist => {
+                  for(var i = 0; i < blocklist.length; i++) {
+                    inst = blocklist[i].domain;
+                    if(inst.indexOf("*") != -1) {
+                      continue;
+                    }
+                    if(instances[inst] == null) {
+                      instances[inst] = new Object();
+                      instances[inst].domain = inst;
+                      instances[inst].reasons = [];
+                    }
+
+                    if(typeof instances[inst].subtrust == "undefined") {
+                      instances[inst].subtrust = 0;
+                    }
+                    if( blocklist[i].comment == null ) {
+                      blocklist[i].comment = "[ No comments provided ]"
+                    }
+                    if( blocklist[i].severity == "silence" ) {
+                      instances[inst].subtrust -= instances[instance].trust * 
+                          parseFloat(document.getElementById("multiplier-suspend").value);
+                      instances[inst].reasons[instance] = "SILENCED: "+blocklist[i].comment;
+                    } else {
+                      instances[inst].subtrust -= instances[instance].trust * 
+                          parseFloat(document.getElementById("multiplier-limit").value);
+                      instances[inst].reasons[instance] = "LIMITED: "+blocklist[i].comment;
+                    }
+                  }
+                  total++;
+              });
+            } catch(e) {
+              console.log(e);
+            }
+          }
+        }
+        // Normalize trust values
+        for(instance in instances) {
+          if(typeof instances[instance].subtrust != "undefined") {
+            instances[instance].trust = instances[instance].subtrust / total;
+            if(instances[instance].trust > 100) { instances[instance].trust = 100; }
+            if(instances[instance].trust < -100) { instances[instance].trust = -100; }
+          }
+        }
+        resetHosts();
+        document.getElementById("processButton").disabled = false;
+      }
+
+      async function assocWithEnemies() {
+        document.getElementById("assocWithEnemies").disabled = true;
+        var total = 0;
+        subLists.aoe = [];
+        for(instance in instances) {
+          if(instances[instance].trust < 0) {
+            try {
+              var c = 0;
+              while(c < 10) {
+                await fetch("https://"+instance+"/api/v1/directory?limit=80&offset=0", { signal: AbortSignal.timeout(5000) })
+                  .then(response => response.json())
+                  .then(directory => {
+                    if(directory.length < 80) {
+                      c = 10;
+                    }
+                    for(var i = 0; i < directory.length; i++) {
+                      inst = directory[i].acct.split("@")[1];
+                      if(inst == null || inst.indexOf("*") != -1) {
+                        continue;
+                      }
+                      if(subLists.aoe[inst] == null) {
+                        subLists.aoe[inst] = new Object();
+                        subLists.aoe[inst].domain = inst;
+                        subLists.aoe[inst].reasons = [];
+                        subLists.aoe[inst].trust = 0;
+                        subLists.aoe[inst].aoesubtrust = 0;
+                      }
+
+                      subLists.aoe[inst].aoesubtrust += parseFloat(instances[instance].trust);
+                      if(subLists.aoe[inst].aoesubtrust < total) {
+                        total = subLists.aoe[inst].aoesubtrust;
+                      }
+                    }
+                });
+                c++;
+              }
+            } catch(e) {
+              console.log(e);
+            }
+          }
+        }
+        // Normalize trust values
+        for(instance in subLists.aoe) {
+          if(typeof subLists.aoe[instance].aoesubtrust != "undefined") {
+            subLists.aoe[instance].trust = (subLists.aoe[instance].aoesubtrust * 100) / total;
+            if(subLists.aoe[instance].trust > 100) { subLists.aoe[instance].trust = 100; }
+            if(subLists.aoe[instance].trust < -100) { subLists.aoe[instance].trust = -100; }
+          }
+        }
+        addTable("aoe", "Associates of Enemies", subLists.aoe);
+        document.getElementById("assocWithEnemies").disabled = false;
+      }
+
+      async function friendsOfFriends() {
+        document.getElementById("friendsOfFriends").disabled = true;
+        var total = 0;
+        subLists.fof = [];
+        for(instance in instances) {
+          if(instances[instance].trust > 0) {
+            try {
+              var c = 0;
+              while(c < 10) { 
+                await fetch("https://"+instance+"/api/v1/directory?limit=80&offset="+(80*c), { signal: AbortSignal.timeout(5000) })
+                  .then(response => response.json())
+                  .then(directory => {
+                    if(directory.length < 80) {
+                      c = 10;
+                    }
+                    for(var i = 0; i < directory.length; i++) {
+                      inst = directory[i].acct.split("@")[1];
+                      if(inst == null || inst.indexOf("*") != -1) {
+                        continue;
+                      }
+                      if(subLists.fof[inst] == null) {
+                        subLists.fof[inst] = new Object();
+                        subLists.fof[inst].domain = inst;
+                        subLists.fof[inst].reasons = [];
+                        subLists.fof[inst].trust = 0;
+                        subLists.fof[inst].fofsubtrust = 0;
+                      }
+
+                      subLists.fof[inst].fofsubtrust += parseFloat(instances[instance].trust);
+                      if(subLists.fof[inst].fofsubtrust > total) {
+                        total = subLists.fof[inst].fofsubtrust;
+                      }
+                    }
+                });
+                c++;
+              }
+            } catch(e) {
+              console.log(e);
+            }
+          }
+        }
+        // Normalize trust values
+        for(instance in subLists.fof) {
+          if(typeof subLists.fof[instance].fofsubtrust != "undefined") {
+            subLists.fof[instance].trust = (int)(subLists.fof[instance].fofsubtrust * 100) / total;
+            if(subLists.fof[instance].trust > 100) { subLists.fof[instance].trust = 100; }
+            if(subLists.fof[instance].trust < -100) { subLists.fof[instance].trust = -100; }
+          }
+        }
+        addTable("fof", "Friends of Friends", subLists.fof);
+        document.getElementById("friendsOfFriends").disabled = false;
+      }
+
+
+      async function enemiesOfEnemies() {
+        document.getElementById("enemiesOfEnemies").disabled = true;
+        var total = 0;
+        subLists.eoe = [];
+        for(instance in instances) {
+          if(instances[instance].trust < 0) {
+            try {
+              await fetch("https://"+instance+"/api/v1/instance/domain_blocks", { signal: AbortSignal.timeout(5000) })
+                .then(response => response.json())
+                .then(blocklist => {
+                  for(var i = 0; i < blocklist.length; i++) {
+                    inst = blocklist[i].domain;
+                    if(inst.indexOf("*") != -1) {
+                      continue;
+                    }
+                    if(subLists.eoe[inst] == null) {
+                      subLists.eoe[inst] = new Object();
+                      subLists.eoe[inst].domain = inst;
+                      subLists.eoe[inst].reasons = [];
+                      subLists.eoe[inst].eoesubtrust = 0;
+                    }
+
+                    if( blocklist[i].comment == null ) {
+                      blocklist[i].comment = "[ No comments provided ]"
+                    }
+                    if( blocklist[i].severity == "silence" ) {
+                      subLists.eoe[inst].eoesubtrust -= instances[instance].trust *
+                          parseFloat(document.getElementById("multiplier-suspend").value);
+                      subLists.eoe[inst].reasons[instance] = "SILENCED: "+blocklist[i].comment;
+                    } else {
+                      subLists.eoe[inst].eoesubtrust -= instances[instance].trust *
+                          parseFloat(document.getElementById("multiplier-limit").value);
+                      subLists.eoe[inst].reasons[instance] = "LIMITED: "+blocklist[i].comment;
+                    }
+                  }
+                  total++;
+              });
+            } catch(e) {
+              console.log(e);
+            }
+          }
+        }
+        // Normalize trust values
+        for(instance in subLists.eoe) {
+          if(typeof subLists.eoe[instance].eoesubtrust != "undefined") {
+            subLists.eoe[instance].trust = subLists.eoe[instance].eoesubtrust / total;
+            if(subLists.eoe[instance].trust > 100) { subLists.eoe[instance].trust = 100; }
+            if(subLists.eoe[instance].trust < -100) { subLists.eoe[instance].trust = -100; }
+          }
+        }
+        addTable("eoe", "Enemies of Enemies", subLists.eoe);
+        document.getElementById("enemiesOfEnemies").disabled = false;
+      }
+
+      function addTable(id, title, data) {
+        var mainDiv = document.getElementById("hosts"+id);
+        if(mainDiv != null) {
+          mainDiv.parentElement.removeChild(mainDiv);
+        }
+        mainDiv = document.createElement("div");
+        mainDiv.setAttribute("id", "hosts"+id);
+        mainDiv.setAttribute("class", "subpanel");
+
+        var button = document.createElement("button");
+        button.setAttribute("id", "hosts"+id+"Open");
+        button.setAttribute("class", "panelButton");
+        button.innerHTML = "+";
+        button.onclick = openPanel;
+        mainDiv.appendChild(button);
+
+        var header = document.createElement("h2");
+        header.innerHTML = title;
+        mainDiv.appendChild(header);
+
+        var listElem = document.createElement("div");
+        listElem.setAttribute("id", "hosts"+id+"List");
+        listElem.setAttribute("class", "list");
+        listElem.setAttribute("listId", id);
+        for(element in data) {
+          var instance = element;
+
+          var instElem = document.createElement("div");
+          instElem.innerHTML = "<h3>" + instance + "</h3>";
+          instElem.setAttribute("id", "instance-"+instance);
+          instElem.setAttribute("class", "instanceElem");
+
+          var commentsElem = document.createElement("div");
+          commentsElem.setAttribute("class", "comments");
+          commentsElem.innerHTML = "";
+          for(inst in data[element].reasons) {
+            commentsElem.innerHTML += inst + " - " + data[element].reasons[inst] + "<br>";
+          }
+          instElem.appendChild(commentsElem);
+
+          var divElem = document.createElement("div");
+          var rangeElem = document.createElement("input");
+          rangeElem.setAttribute("type", "range");
+          rangeElem.setAttribute("min",  "-100");
+          rangeElem.setAttribute("max",  "100");
+          rangeElem.setAttribute("value",data[element].trust);
+          rangeElem.oninput = syncRange;
+          rangeElem.onchange = updateRange;
+          divElem.appendChild(rangeElem);
+
+          var valElem = document.createElement("input");
+          valElem.setAttribute("type", "text");
+          valElem.value = data[element].trust;
+          valElem.onchange = updateVal;
+          divElem.appendChild(valElem);
+          instElem.appendChild(divElem);
+
+          addButton = document.createElement("button");
+          addButton.onclick = subListAdd;
+          addButton.innerHTML = "Add";
+          instElem.appendChild(addButton);
+
+          listElem.appendChild(instElem);
+        }
+        mainDiv.appendChild(listElem);
+
+        document.getElementById("hostsPanel").appendChild(mainDiv);
+      }
+
+      function subListAdd(event) {
+        var instance = event.target.parentElement.children[0].innerText;
+        var id = event.target.parentElement.parentElement.getAttribute("listId");
+
+        // TODO: Check for confirmation if it already exists
+        instances[instance] = subLists[id][instance];
+        event.target.parentElement.parentElement.removeChild(event.target.parentElement);
+        resetHosts();
+      }
+
+      function resetHosts() {
+        document.getElementById("hostsTrustedList").innerHTML = "<div><h3>Instance</h3><div class='comments'></div><div><h3>Trust Value</h3></div></div>";
+        document.getElementById("hostsUntrustedList").innerHTML = "<div><h3>Instance</h3><div class='comments'></div><div><h3>Trust Value</h3></div></div>";
+        document.getElementById("hostsKnownList").innerHTML = "<div><h3>Instance</h3><div class='comments'></div><div><h3>Trust Value</h3></div></div>";
+        var instanceKeys = Object.keys(instances).sort();
+        for(var i = 0; i < instanceKeys.length; i++) {
+          var instance = instanceKeys[i]; //.domain; //.acct.replace( directory[i].username + "@", "" );
+          if(instances[instance].trust == undefined) {
+            instances[instance].trust = 0;
+          }
+
+          var instElem = document.createElement("div");
+          instElem.innerHTML = "<h3>" + instance + "</h3>";
+          instElem.setAttribute("id", "instance-"+instance);
+          instElem.setAttribute("class", "instanceElem");
+
+          var commentsElem = document.createElement("div");
+          commentsElem.setAttribute("class", "comments");
+          commentsElem.innerHTML = "";
+          for(inst in instances[instance].reasons) {
+            commentsElem.innerHTML += inst + " - " + instances[instance].reasons[inst] + "<br/>";
+          }
+          instElem.appendChild(commentsElem);
+
+          var divElem = document.createElement("div");
+          var rangeElem = document.createElement("input");
+          rangeElem.setAttribute("type", "range");
+          rangeElem.setAttribute("min",  "-100");
+          rangeElem.setAttribute("max",  "100");
+          rangeElem.setAttribute("value",instances[instance].trust);
+          rangeElem.oninput = syncRange;
+          rangeElem.onchange = updateRange;
+          divElem.appendChild(rangeElem);
+
+          var valElem = document.createElement("input");
+          valElem.setAttribute("type", "text");
+          valElem.value = instances[instance].trust;
+//          valElem.disabled = true;
+          valElem.onchange = updateVal;
+          divElem.appendChild(valElem);
+          instElem.appendChild(divElem);
+
+/*          var headerDiv = document.createElement("div");
+          headerDiv.innerHTML = "<h3>Instance</h3><div><h3>Trust Value</h3></div>";
+          document.getElementById("hostsTrustedList").innerHTML = "";
+          document.getElementById("hostsUntrustedList").innerHTML = "";*/
+          if(instances[instance].trust == 0) {
+            document.getElementById("hostsKnownList").appendChild(instElem);
+          } else if(instances[instance].trust > 0) {
+            document.getElementById("hostsTrustedList").appendChild(instElem);
+          } else {
+            document.getElementById("hostsUntrustedList").appendChild(instElem);
+          } 
+        }
+//        document.getElementById("hostsKnownOpen").onclick();
+      }
+
+      function openPanel(event) {
+        var listElem = this.parentElement.getElementsByClassName("list")[0];
+        if( listElem.style.display == "block" ) {
+          this.parentElement.getElementsByClassName("list")[0].style.display = "none";
+          this.innerHTML = "+";
+        } else {
+          this.parentElement.getElementsByClassName("list")[0].style.display = "block";
+          this.innerHTML = "-";
+        }
+      }
+
+      function updateRange(event) {
+        var id = this.parentElement.parentElement.parentElement.getAttribute("listId");
+        var instance = this.parentElement.parentElement.firstChild.innerText;
+        if( id == null) {
+          instances[instance].trust = this.value;
+        } else {
+          subLists[id][instance].trust = this.value;
+        }
+        this.parentElement.children[1].value = this.value;
+
+        if(id == null) {
+          var parentPanel = this.parentElement.parentElement.parentElement;
+          var instElem = this.parentElement.parentElement;
+          if( this.value < 0 && parentPanel != document.getElementById("hostsUntrustedList") ) {
+            parentPanel.removeChild(instElem);
+            document.getElementById("hostsUntrustedList").appendChild(instElem);
+          } else if( this.value > 0 && parentPanel != document.getElementById("hostsTrustedList") ) {
+            parentPanel.removeChild(instElem);
+            document.getElementById("hostsTrustedList").appendChild(instElem);
+          } else if( this.value == 0 && parentPanel != document.getElementById("hostsKnownList") ) {
+            parentPanel.removeChild(instElem);
+            document.getElementById("hostsKnownList").appendChild(instElem);
+          }
+        }
+      }
+
+      function syncRange(event) {
+        this.parentElement.children[1].value = this.value;
+      }
+
+      function updateVal(event) {
+        var id = event.target.parentElement.parentElement.getAttribute("listId");
+        var instance = this.parentElement.parentElement.firstChild.innerText;
+        if(this.value < -100) { this.value = -100; }
+        if(this.value > 100) { this.value = 100; }
+        this.parentElement.children[0].value = this.value;
+        instances[instance].trust = this.value;
+
+        if(id == null) {
+          var parentPanel = this.parentElement.parentElement.parentElement;
+          var instElem = this.parentElement.parentElement;
+          if( this.value < 0 && parentPanel != document.getElementById("hostsUntrustedList") ) {
+            parentPanel.removeChild(instElem);
+            document.getElementById("hostsUntrustedList").appendChild(instElem);
+          } else if( this.value > 0 && parentPanel != document.getElementById("hostsTrustedList") ) {
+            parentPanel.removeChild(instElem);
+            document.getElementById("hostsTrustedList").appendChild(instElem);
+          } else if( this.value == 0 && parentPanel != document.getElementById("hostsKnownList") ) {
+            parentPanel.removeChild(instElem);
+            document.getElementById("hostsKnownList").appendChild(instElem);
+          }
+        }
+      }
+
+      function importStr(str = "") {
+        var shade = document.createElement("div");
+        shade.setAttribute("id", "shade");
+        shade.onclick = function() {
+          document.body.removeChild(document.getElementById("shade"));
+          document.body.removeChild(document.getElementById("shade-text"));
+          document.body.removeChild(document.getElementById("shade-text-button"));
+        }
+
+        var text  = document.createElement("textarea");
+        text.innerHTML = str;
+        text.setAttribute("id", "shade-text");
+
+        var btn = document.createElement("button");
+        btn.innerHTML = "Import";
+        btn.setAttribute("id", "shade-text-button");
+        btn.onclick = function() {
+          loadStr(document.getElementById("shade-text").value);
+          document.getElementById("shade").onclick();
+          resetHosts();
+        };
+
+        document.body.appendChild(shade);
+        document.body.appendChild(text);
+        document.body.appendChild(btn);
+      }
+
+      function saveStr() {
+        var instancesArr = [];
+        for(instance in instances) {
+          instancesArr.push(instances[instance]);
+          if(typeof instances[instance].reasons == "object") { 
+            reasonsExport = [];
+            for(inst in instances[instance].reasons) {
+              var rsn = {};
+              rsn.domain = inst;
+              rsn.text = instances[instance].reasons[inst];
+              reasonsExport.push(rsn);
+            }
+            instancesArr[instancesArr.length-1].reasons = reasonsExport;
+          }
+        }
+
+        var configArr = {};
+        configArr.instances = instancesArr;
+        configArr.srchost = document.getElementById("sourceHost").value;
+        configArr.mulLim  = document.getElementById("multiplier-limit").value;
+        configArr.mulSus  = document.getElementById("multiplier-suspend").value;
+        configArr.thrLim  = document.getElementById("threshold-limit").value;
+        configArr.thrSus  = document.getElementById("threshold-suspend").value;
+        return JSON.stringify(configArr);
+      }
+      function loadStr(configJSON) {
+        instances = [];
+        configArr = JSON.parse(configJSON);
+        document.getElementById("sourceHost").value = configArr.srchost;
+        document.getElementById("multiplier-limit").value  = configArr.mulLim;
+        document.getElementById("multiplier-suspend").value= configArr.mulSus;
+        document.getElementById("threshold-limit").value   = configArr.thrLim;
+        document.getElementById("threshold-suspend").value = configArr.thrSus;
+        for(var i = 0; i < configArr.instances.length; i++) {
+          instances[configArr.instances[i].domain] = configArr.instances[i];
+          if(typeof configArr.instances[i].reasons == "object") {
+            reasonsArr = [];
+            for(j = 0; j < configArr.instances[i].reasons.length; j++) {
+              reasonsArr[configArr.instances[i].reasons[j].domain] = 
+                configArr.instances[i].reasons[j].text;
+            }
+            configArr.instances[i].reasons = reasonsArr;
+          }
+        }
+      }
+
+      function exportStr() {
+        importStr(saveStr());
+      }
+
+      function loadCache() {
+        loadStr(localStorage.getItem("config"));
+        resetHosts();        
+      }
+
+      function saveCache() {
+        localStorage.setItem("config", saveStr());
+      }
+
+      function checkImpotence(hostList, instance, response = null) {
+        if(typeof instances[instance].impotent != "undefined" && hostList.length > 0) {
+          instance = hostList.pop();
+          checkImpotence(hostList, instance, response);
+        }
+        if(response != null) {
+          if(response.status == "404") {
+            instances[instance].impotent = 1;
+            if(instances[instance].impotent == 1) {
+              document.getElementById("instance-"+instance).style.display = "none";
+            } else {
+              document.getElementById("instance-"+instance).style.display = "block";
+            }
+          } else if(response.status == "200") {
+            instances[instance].impotent = 0;
+            if(instances[instance].impotent == 1) {
+              document.getElementById("instance-"+instance).style.display = "none";
+            } else {
+              document.getElementById("instance-"+instance).style.display = "block";
+            }
+          } else {
+            instances[instance].impotent = 1;
+            if(instances[instance].impotent == 1) {
+              document.getElementById("instance-"+instance).style.display = "none";
+            } else {
+              document.getElementById("instance-"+instance).style.display = "block";
+            }
+          }
+        }
+
+        if(hostList.length == 0) {
+          return;
+        }
+        var instance = hostList.pop();
+        fetch("https://"+instance+"/api/v1/instance/domain_blocks", { signal: AbortSignal.timeout(5000) })
+          .then(response => checkImpotence(hostList, instance, response))
+          .catch(response => checkImpotence(hostList, instance, response));
+      }
+
+      function hideImpotent() {
+        var keys = Object.keys(instances);
+        for(i = 0; i < keys.length; i+=(keys.length/10)) {
+          var sublist = keys.slice(i, i+(keys.length/10));
+          var instance = sublist.pop();
+          checkImpotence( sublist, instance )
+        }
+      }
+
+      async function hideSubdomains() {
+        alert("STUB");
+      }
+
+      function exportCSV() {
+        var data = "";
+        for(instance in instances) {
+          var action = "";
+          if(instances[instance].trust <= document.getElementById('threshold-limit').value) {
+            action = "limit";
+          } else if(instances[instance].trust <= document.getElementById('threshold-suspend').value) {
+            action = "suspend";
+          }
+
+          if(action != "") {
+            data += instance + ",";
+            data += action + ",";
+            data += '"';
+            for(inst in instances[instance].reasons) {
+              data += inst + " - " + instances[instance].reasons[inst] + "\n";
+            }
+            data = data.trim();
+            data += '"\n';
+          }
+        }
+        var elem = document.createElement("a");
+        elem.setAttribute("href", "data:text/plain;charset=utf-8," 
+          + encodeURIComponent(data.trim()));
+        elem.setAttribute('download', 'mastowot.csv');
+        document.body.appendChild(elem);
+        elem.click();
+        document.body.removeChild(elem);
+      }
+    </script>
+  </head>
+  <body>
+    <h1>MastoWoT: A Web-of-Trust inspired defederation station</h1>
+    <!-- Constant iteration of blacklist generation for spam immoliation, 
+           without subjugation, now with extra alliteration! -->
+    <!-- Although likely just mental masturbation becaues really is 
+           anyone going to use this thing? I doubt it... -->
+    <div id="manPanel">
+      <button id="manPanelOpen" class="panelButton">-</button>
+      <h2>Instructions</h2>
+      <div class="list" style="display: block"> 
+        <p><strong>This tool is intended for Mastodon instance administrators</strong></p>
+        <p>You can still play around with it even if you aren't a server admin, but 
+           don't complain to me that you can't figure out how to work it :)</p>
+        <p>This tool is designed to <em>suggest</em> a blocklist that you may
+           want to configure on your Mastodon instance. The goal is to create a 
+           system simpler than scrolling #FediBlock, more proactive than relying
+           entirely on user reports, and more federated than distributing blocklists
+           from a central authority.</p>
+         <p>To start, enter your instance's domain (no 'https://', no page, just the domain)
+           into the box above the synchronize button
+           on the right side menu, and click "Synchronize". This will make two calls
+           to your server's API to fetch the known peers and the block list. This 
+           WILL require that your instance is configured to share this information. 
+           This can be configured in your instance settings, under Site Settings, 
+           enable 'publish list of discovered servers' and 'enable profile directory'.
+           Also set "show domain blocks" and "show rationale" as "to everyone".</p>
+          <p>Once you synchronie, any domains you have already blocked  will appear 
+            under Untrusted Hosts. All other known peers will appear under Known Hosts.
+            Now, you need to find some hosts you trust in the Known Hosts list and move
+            their slider to the right. The slider moves between -100 and 100, and once
+            you select a value it will update in the text box (you can also enter a
+            value into the box directly). This should
+            represent how much you "trust" this server, with -100 meaning you think this
+            server is "evil" and 100 meaning it is to be trusted entirely (at least for
+            the purposes of generating a blocklist). As you set values, these hosts will
+            move to Trusted Hosts or Untrusted Hosts accordingly. I do NOT recommend
+            trying to set a value for every single instance in the list. Search the page
+            for instances that you are already familiar with instead.</p>
+          <p>Once you have added a few trusted instances, click "Process" from the right
+            side menu. This will fetch the block list from each instance on your trusted
+            list. A host that appears on any of these blocklists will receive a negative 
+            trust value equal to the trust value of the hosts that have blocked it,
+            multiplied by the appropriate multiplier values (configured in the side 
+            menu), averaged by dividing it by the total number of hosts in the list.</p>
+           <p>Finally, you can use the "Export for Mastodon" button from the side menu
+             to export a blocklist as a CSV file, which you can then import into your
+             instance using the script provided below. This export will be
+             generated based on the Thresholds values set on the right side menu. Any
+             hosts with a trust value equal to or below the suspend threshold will be
+             suspended; any remaining hosts with a trust value equal to or below the 
+             limit thershold will be limited.</p>
+           <p>Also please be aware that some instances may choose to obfuscate certain
+             URLs from their block list. Please don't do that. <strong>This tool cannot
+             work with obfuscated URLs.</strong></p>
+           <p>The remaining options are mentioned below:</p>
+           <ul>
+             <li>Hide Impotents: Tries to fetch a blocklist from all hosts in the list.
+               If no blocklist can be downloaded, the host will be hidden (They will NOT
+               be removed from the list, only hidden from display.) There is no point
+               adding one of these hosts to your trusted list if they do not provide a
+               their block list.</li>
+             <li>Find Enemies of Enemies: Creates a new list of hosts that are blocked
+               by the hosts on your untrusted list. The hosts on this list will NOT be
+               considered trusted or untrusted until you click the 'add' button to move
+               them into the main list with the given trust value.</li>
+             <li>Find Friends of Friends: Creates a new list of hosts that are in the
+               directory of the hosts in your trusted list. Trust values will be based
+               on both the trust value of the trusted host and the number of accounts
+               in its directory that are hosted at the this instance.</li>
+             <li>FInd Associates of Enemies: Creates a new list of hosts that are in the
+               directory of the hosts in your untrusted list. Trust values will be based
+               on both the trust value of the untrusted host and the number of accounts
+               in its directory that are hosted at the this instance.</li>
+             <li>Load from cache: Load instances list from the browser cache</li>
+             <li>Save to cache: Save instances list to your browser cache</li>
+             <li>Export to you: Show the instances list as a JSON string</li>
+             <li>Import from you: Import a JSON string exported previously</li>
+           </ul>
+
+<hr/>
+<pre>
+# IMPORT SCRIPT
+# I realize Mastodon already has an import function, but as far as I can tell, that
+#   does not allow you to include the comments. This script will.
+
+# This script assumes the export file is available in the current directory 
+#   and named 'mastowot.csv'
+
+# This should be your mastodon API token with admin access
+# (Generate from your admin user's development menu. Requires admin;write permissions)
+ADMIN_ACCESS_TOKEN=""
+# And your instance hostname of course
+MASTODON_HOST="https://"
+
+ts="`date +%Y%m%d-%H:%M:%S`"
+awk -v RS='"\n' \
+    -v FS="," \
+    -v authtoken="$ADMIN_ACCESS_TOKEN" \
+    -v tgthost="$MASTODON_HOST" \
+    -v timestamp="$ts" \
+'{
+    system("curl -X POST -H \"Authorization: Bearer "authtoken"\" \
+              -F \"domain="$1"\" -F \"private_comment=Updated by MastoWoT "timestamp"\" \
+              -F \"public_comment="$2"\" \
+              "tgthost"/api/v1/admin/domain_blocks");
+}' mastowot.csv
+
+</pre>
+<hr/>
+           <pre>
+TODO:
+  Styling:
+    Color chooser, high contrast mode
+  Sort lists by name or trust level
+  Determine if blocked hosts have/had direct associations? and when?
+
+</pre>
+           <p>Ultimately I do think this would make more sense as a shell script run
+             via cron job...but I think this is a better way to display the concept,
+             so that version is part two :)</p>
+      </div>
+    </div>
+    <div id="hostsPanel">
+      <div id="hostsTrusted" class="subpanel">
+        <button id="hostsTrustedOpen" class="panelButton">+</button>
+        <h2>Trusted Hosts</h2>
+        <div id="hostsTrustedList" class="list">
+          <div>
+            <h3>Instance</h3>
+            <div>
+              <h3>Trust Value</h3>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div id="hostsUntrusted" class="subpanel">
+        <button id="hostsUntrustedOpen" class="panelButton">+</button>
+        <h2>Untrusted Hosts</h2>
+        <div id="hostsUntrustedList" class="list">
+          <div>
+            <h3>Instance</h3>
+            <div>
+              <h3>Trust Value</h3>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div id="hostsKnown" class="subpanel">
+        <button id="hostsKnownOpen" class="panelButton">+</button>
+        <h2>Known Hosts</h2>
+        <div id="hostsKnownList" class="list">
+          <div>
+            <h3>Instance</h3>
+            <div>
+              <h3>Trust Value</h3>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div id="settingsPanel">
+      <input type="text" id="sourceHost" />
+      <button id="syncButton" onclick="synchronize()">Synchronize</button>
+      <br/>
+      <button id="processButton" onclick="process()">Process</button>
+      <br/>
+      <button onclick="hideImpotent()" >Hide impotents</button>
+      <button id="enemiesOfEnemies" onclick="enemiesOfEnemies()">Find Enemies of Enemies</button>
+      <button id="friendsOfFriends" onclick="friendsOfFriends()">Find Friends of Friends</button>
+      <button id="assocWithEnemies" onclick="assocWithEnemies()">Find Associates of Enemies</button>
+      <br/>
+      <button onclick="loadCache()" >Load from cache</button>
+      <button onclick="saveCache()" >Save to cache</button>
+      <br/>
+      <button onclick="exportStr()" >Export to you</button>
+      <button onclick="importStr()" >Import from you</button>
+      <br/>
+      <button onclick="exportCSV()" >Export for Mastodon</button>
+      <br/>
+<!--      <button onclick="hideSubdomains()" >Hide Subdomains</button>  -->
+<!--      <button>Share</button> -->
+      <div>
+        <h2>Multipliers</h2>
+        <label>Limit</label>
+        <input type="text" value="1" id="multiplier-limit" />
+        <label>Suspend</label>
+        <input type="text" value="2" id="multiplier-suspend" />
+      </div>
+      <div>
+        <h2>Thresholds</h2>
+        <label>Limit</label>
+        <input type="text" value="-50" id="threshold-limit" />
+        <label>Suspend</label>
+        <input type="text" value="-100" id="threshold-suspend" />
+      </div>
+    </div>
+    <script>init();</script>
+  </body>
+</html>
diff --git a/mastowot.css b/mastowot.css
new file mode 100644 (file)
index 0000000..734bba4
--- /dev/null
@@ -0,0 +1,181 @@
+body {
+  background-color: black;
+  padding: 0px;
+  margin:  0px;
+  color:   #CAA;
+  font-family: "Audiowide", "Sans";
+}
+
+h1 {
+  width: 100%;
+  text-style: italic;
+  text-align: center;
+}
+
+h2 {
+  display: inline-block;
+  margin:  .3em .5em .5em .5em;
+  vertical-align: middle;
+}
+
+.panelbutton {
+  display: inline-block;
+}
+
+#settingsPanel #sourceHost {
+  display: inline-block;
+  width: 100%;
+}
+
+#hostsPanel {
+  display:   inline-block;
+  width:     calc( 70% - 10px );
+  min-width: 30em;
+  height:    calc(100% - 3em);
+  margin: 0px;
+  vertical-align: top;
+}
+
+.subpanel, #manPanel {
+  padding: 0em 1em;
+  border:  3px solid #C00;
+  border-radius:    1em;
+  background-color: #600;
+  margin-bottom: 1em;
+}
+
+#settingsPanel {
+  display:   inline-block;
+  width:     calc( 30% - 10px - 2em);
+  min-width: 10em;
+  height:    calc(100% - 2em);
+  background-color: #400;
+  border: 3px solid #C00;
+  margin: 0px;
+  border-radius: 1em;
+  padding: 1em;
+  vertical-align: top;
+}
+
+#settingsPanel a {
+  display: block;
+  width: 100%;
+  text-align: center;
+  margin-bottom: 1em;
+  font-weight: bold;
+  color: #C00;
+}
+
+#settingsPanel button {
+  display: block;
+  width: 100%;
+}
+
+#settingsPanel div {
+  border: 1px solid black;
+  border-radius: 1em;
+  padding: 0em 0em 1em 0em;
+  margin-top: 1em;
+}
+
+#settingsPanel h2 {
+  width: 100%;
+}
+
+#settingsPanel label {
+  margin-left: 10%;
+  display: inline-block;
+  width: 40%;
+}
+
+#settingsPanel input {
+  display: inline-block;
+  width: 40%;
+}
+
+#shade {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  background-color: black;
+  opacity: 0.8;
+  z-index: 100;
+  left: 0px;
+  top: 0px;
+}
+
+#shade-text {
+  position: fixed;
+  width: 40%;
+  height: 40%;
+  left: 30%;
+  top: 30%;
+  z-index: 150;
+}
+
+#shade-text-button {
+  position: fixed;
+  width: 40%;
+  height: 5%;
+  left: 30%;
+  top: 70%;
+  z-index: 150;
+  font-size: 3vh;
+}
+
+.subpanel .list {
+  display: none;
+}
+
+.subpanel .list .instanceElem:nth-child(2n+3) {
+  background-color: #C11;
+}
+
+.subpanel .list div {
+  display: block;
+  position: relative;
+  border-bottom: 1px solid black;
+}
+
+.subpanel .list div h3 {
+  display: inline-block;
+  width: calc( 50% - 20em );
+  margin: 0em;
+  padding: 0em;
+  overflow: scroll;
+}
+
+.subpanel .list div div {
+  display: inline-block;
+  position: relative;
+  width: 20em;
+  vertical-align: middle;
+}
+
+.subpanel .list div .comments {
+  display: inline-block;
+  margin: none;
+  width: 48%;
+  border: none;
+}
+
+.subpanel .list div div input[type=text] {
+  display: inline-block;
+  width: 3em;
+  vertical-align: middle;
+}
+
+.subpanel .list div div input[type=range] {
+  display: inline-block;
+  width: 17em;
+  vertical-align: middle;
+}
+
+.subpanel .list div div h3 {
+  width: 100%;
+}
+
+.subpanel .list div:first-child h3 {
+  font-weight: bold;
+  text-decoration: underline;
+}
diff --git a/mastowot.html b/mastowot.html
new file mode 100644 (file)
index 0000000..810dd19
--- /dev/null
@@ -0,0 +1,227 @@
+<html>
+  <head>
+    <title>MastoWoT</title>
+    <link rel="stylesheet" href="./mastowot.css" type="text/css" />
+    <script src="./mastowot.js" type="text/javascript"></script>
+    <!-- NOTE TO DEVS AND SUCH: I'll probably add an open source license to this and
+         get it in my git repo at some point, but at the moment I have not done that.
+         If you'd like to share/modify/etc, please let me know.
+         @admin@mastodon.slightlycyberpunk.com -->
+  </head>
+  <body>
+    <h1>MastoWoT: A Web-of-Trust inspired defederation station</h1>
+    <!-- Constant iteration of blacklist generation for spam immoliation, 
+           without subjugation, now with extra alliteration! -->
+    <!-- Although likely just mental masturbation becaues really is 
+           anyone going to use this thing? I doubt it... -->
+
+    <div id="manPanel">
+      <button id="manPanelOpen" class="panelButton">-</button>
+      <h2>Instructions</h2>
+      <div class="list" style="display: block"> 
+        <p><strong>This tool is intended for Mastodon instance administrators</strong></p>
+        <p>You can still play around with it even if you aren't a server admin, but 
+           don't complain to me that you can't figure out how to import the lists :)</p>
+        <p>This tool is designed to <em>suggest</em> a blocklist that you may
+           want to configure on your Mastodon instance. The goal is to create a 
+           system simpler than scrolling #FediBlock, more proactive than relying
+           entirely on user reports, and more federated than distributing blocklists
+           from a central authority.</p>
+         <p>To start, enter your instance's domain (no 'https://', no page, just the domain)
+           into the box above the synchronize button
+           on the right side menu, and click "Synchronize". This will make two calls
+           to your server's API to fetch the known peers and the block list. This 
+           WILL require that your instance is configured to share this information. 
+           This can be configured in your instance settings, under Site Settings, 
+           enable 'publish list of discovered servers' and 'enable profile directory'.
+           Also set "show domain blocks" and "show rationale" as "to everyone".</p>
+          <p>Once you synchronize, any domains you have already blocked  will appear 
+            under Untrusted Hosts. All other known peers will appear under Known Hosts.
+            Now, you need to find some hosts you trust in the Known Hosts list and move
+            their slider to the right. The slider moves between -100 and 100, and once
+            you select a value it will update in the text box (you can also enter a
+            value into the box directly). This should
+            represent how much you "trust" this server, with -100 meaning you think this
+            server is "evil" and 100 meaning it is to be trusted entirely (at least for
+            the purposes of generating a blocklist). As you set values, these hosts will
+            move to Trusted Hosts or Untrusted Hosts accordingly. I do NOT recommend
+            trying to set a value for every single instance in the list. Search the page
+            for instances that you are already familiar with instead.</p>
+          <p>Once you have added a few trusted instances, click "Process" from the right
+            side menu. This will fetch the block list from each instance on your trusted
+            list. A host that appears on any of these blocklists will receive a negative 
+            trust value equal to the trust value of the hosts that have blocked it,
+            multiplied by the appropriate multiplier values (configured in the side 
+            menu), averaged by dividing it by the total number of hosts in the list.</p>
+           <p>Finally, you can use the "Export for Mastodon" button from the side menu
+             to export a blocklist as a CSV file, which you can then import into your
+             instance using the script provided below. This export will be
+             generated based on the Thresholds values set on the right side menu. Any
+             hosts with a trust value equal to or below the suspend threshold will be
+             suspended; any remaining hosts with a trust value equal to or below the 
+             limit thershold will be limited.</p>
+           <p>Also please be aware that some instances may choose to obfuscate certain
+             URLs from their block list. Please don't do that. <strong>This tool cannot
+             work with obfuscated URLs.</strong></p>
+           <p>The remaining options are mentioned below:</p>
+           <ul>
+             <li>Hide Impotents: Tries to fetch a blocklist from all hosts in the list.
+               If no blocklist can be downloaded, the host will be hidden (They will NOT
+               be removed from the list, only hidden from display.) There is no point
+               adding one of these hosts to your trusted list if they do not provide a
+               their block list.</li>
+             <li>Hide Subdomains: Hides any instances from the list which are subdomains
+               of another entry in the list. Blocking the parent domain blocks all
+               subdomains, so you may not always need to handle each subdomain separately.</li>
+             <li>Find Enemies of Enemies: Creates a new list of hosts that are blocked
+               by the hosts on your untrusted list. The hosts on this list will NOT be
+               considered trusted or untrusted until you click the 'add' button to move
+               them into the main list with the given trust value.</li>
+             <li>Find Associates of Enemies: Creates a new list of hosts that are in the
+               directory of the hosts in your untrusted list. Trust values will be based
+               on both the trust value of the untrusted host and the number of accounts
+               in its directory that are hosted at the this instance. (This will take a
+               while to process.)</li>
+             <li>Find Associates of Friends: Creates a new list of hosts that are in the
+               directory of the hosts in your trusted list. Trust values will be based
+               on both the trust value of the trusted host and the number of accounts
+               in its directory that are hosted at the this instance. (This will take a
+               while to process.)</li>
+             <li>Load from cache: Load instances list from the browser cache</li>
+             <li>Save to cache: Save instances list to your browser cache</li>
+             <li>Export to you: Show the instances list as a JSON string</li>
+             <li>Import from you: Import a JSON string exported previously</li>
+           </ul>
+
+<hr/>
+<pre>
+# IMPORT SCRIPT
+# I realize Mastodon already has an import function, but as far as I can tell, that
+#   does not allow you to include the comments. This script will.
+
+# This script assumes the export file is available in the current directory 
+#   and named 'mastowot.csv'
+
+# This should be your mastodon API token with admin access
+# (Generate from your admin user's development menu. Requires admin;write permissions)
+ADMIN_ACCESS_TOKEN=""
+# And your instance hostname of course
+MASTODON_HOST="https://"
+
+ts="`date +%Y%m%d-%H:%M:%S`"
+awk -v RS='"\n' \
+    -v FS="," \
+    -v authtoken="$ADMIN_ACCESS_TOKEN" \
+    -v tgthost="$MASTODON_HOST" \
+    -v timestamp="$ts" \
+'{
+    system("curl -X POST -H \"Authorization: Bearer "authtoken"\" \
+              -F \"domain="$1"\" -F \"private_comment=Updated by MastoWoT "timestamp"\" \
+              -F \"public_comment="$2"\" \
+              "tgthost"/api/v1/admin/domain_blocks");
+}' mastowot.csv
+
+</pre>
+<hr/>
+           <pre>
+TODO:
+  Styling:
+    Color chooser, high contrast mode
+    Maybe make the UI not look like....this.
+  Determine if blocked hosts have/had direct associations, and when?
+    (ie, see if they blocked this person or if they got the block from elsewhere)
+  Explain how this shit works a little better, maybe a video?
+    (Maybe a *good* video...)
+</pre>
+           <p>[ Ultimately I do think this would make more sense as a shell script run
+             via cron job...but I think this is a better way to display the concept,
+             so that version is part two :) ]</p>
+           <p>FYI, this site runs entirely on your machine and should not send ANY of
+             your data to my server. This page is made of three files -- mastowot.html,
+             mastowot.js, and mastowot.css. You can download all three and run them from
+             your local system, no server required. You can copy them to your own server
+             to share. They are licensed under the GPL v3, and about 1000 lines of code
+             last I checked if you want to review it.</p>
+      </div>
+    </div>
+    <div id="hostsPanel">
+      <div id="hostsTrusted" class="subpanel">
+        <button id="hostsTrustedOpen" class="panelButton">+</button>
+        <h2>Trusted Hosts</h2>
+        <div id="hostsTrustedList" class="list">
+          <div>
+            <h3>Instance</h3>
+            <div>
+              <h3>Trust Value</h3>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div id="hostsUntrusted" class="subpanel">
+        <button id="hostsUntrustedOpen" class="panelButton">+</button>
+        <h2>Untrusted Hosts</h2>
+        <div id="hostsUntrustedList" class="list">
+          <div>
+            <h3>Instance</h3>
+            <div>
+              <h3>Trust Value</h3>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div id="hostsKnown" class="subpanel">
+        <button id="hostsKnownOpen" class="panelButton">+</button>
+        <h2>Known Hosts</h2>
+        <div id="hostsKnownList" class="list">
+          <div>
+            <h3>Instance</h3>
+            <div>
+              <h3>Trust Value</h3>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div id="settingsPanel">
+      <input type="text" id="sourceHost" />
+      <button id="syncButton" onclick="synchronize()">Synchronize</button>
+      <br/>
+      <button id="processButton" onclick="process()">Process</button>
+      <br/>
+      <button onclick="hideImpotent()" >Hide Impotents</button>
+      <button onclick="hideSubdomains()" >Hide Subdomains</button>
+      <button id="enemiesOfEnemies" onclick="enemiesOfEnemies()">Find Enemies of Enemies</button>
+      <button id="assocWithEnemies" onclick="assocWithEnemies()">Find Associates of Enemies</button>
+      <button id="friendsOfFriends" onclick="friendsOfFriends()">Find Associates of Friends</button>
+      <br/>
+      <button onclick="loadCache()" >Load from cache</button>
+      <button onclick="saveCache()" >Save to cache</button>
+      <br/>
+      <button onclick="exportStr()" >Export to you</button>
+      <button onclick="importStr()" >Import from you</button>
+      <br/>
+      <button onclick="exportCSV()" >Export for Mastodon</button>
+      <br/>
+<!--      <button onclick="hideSubdomains()" >Hide Subdomains</button>  -->
+<!--      <button>Share</button> -->
+      <div>
+        <h2>Multipliers</h2>
+        <label>Limit</label>
+        <input type="text" value="1" id="multiplier-limit" />
+        <label>Suspend</label>
+        <input type="text" value="2" id="multiplier-suspend" />
+      </div>
+      <div>
+        <h2>Thresholds</h2>
+        <label>Limit</label>
+        <input type="text" value="-50" id="threshold-limit" />
+        <label>Suspend</label>
+        <input type="text" value="-100" id="threshold-suspend" />
+      </div>
+    </div>
+    <script>init();</script>
+  </body>
+</html>
diff --git a/mastowot.js b/mastowot.js
new file mode 100644 (file)
index 0000000..71b9526
--- /dev/null
@@ -0,0 +1,776 @@
+/* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
+ * mastowot.js
+ * version 0.2
+ * updated 2023-01-17
+ * developed by Slightly Cyberpunk 
+ *   mastodon: @admin@mastodon.slightlycyberpunk.com
+ * Copyright 2023 by Slightly Cyberpunk
+ * Released under the GPL v3 only
+ */
+
+/* 
+ * INITIALIZATIONS
+ */
+var instances = [];        // Main list of instances (trusted/untrusted/known)
+var subLists = [];         // Additional lists (EOE, AOE, FOF, etc)
+var sortFuncArr = [];      // Retain the sorting information for each list
+
+function init() {
+  var buttons = document.getElementsByClassName("panelButton");
+  for(var i = 0; i < buttons.length; i++) {
+    buttons[i].onclick = openPanel;
+  }
+}
+
+/*
+ * SUPPORTING FUNCTIONS
+ */
+
+// Sort a list alphabetically by domain name
+function sortDomains(direction, a, b) {
+  if(direction > 0) {
+    return a > b;
+  } else {
+    return a < b;
+  }
+}
+
+// Sort a list by trust level
+// This one takes a list as input because we store by instance name
+// So you can sort by domain just by comparing the key -- it is the domain
+// But to sort by trust level you have to look up the value for that key
+// So you have to know which list it is in
+function sortTrust(direction, list, a, b) {
+  return (list[a].trust - list[b].trust) * direction;
+}
+
+// Create or load the string representation used to import/export
+// JSON doesn't really handle associative arrays
+// So those have to be converted first
+function saveStr() {
+  var instancesArr = [];
+  for(instance in instances) {
+    instancesArr.push(instances[instance]);
+    if(typeof instances[instance].reasons == "object") { 
+      reasonsExport = [];
+      for(inst in instances[instance].reasons) {
+        var rsn = {};
+        rsn.domain = inst;
+        rsn.text = instances[instance].reasons[inst];
+        reasonsExport.push(rsn);
+      }
+      instancesArr[instancesArr.length-1].reasons = reasonsExport;
+    }
+  }
+
+  var configArr = {};
+  configArr.instances = instancesArr;
+  configArr.srchost = document.getElementById("sourceHost").value;
+  configArr.mulLim  = document.getElementById("multiplier-limit").value;
+  configArr.mulSus  = document.getElementById("multiplier-suspend").value;
+  configArr.thrLim  = document.getElementById("threshold-limit").value;
+  configArr.thrSus  = document.getElementById("threshold-suspend").value;
+  return JSON.stringify(configArr);
+}
+function loadStr(configJSON) {
+  instances = [];
+  configArr = JSON.parse(configJSON);
+  document.getElementById("sourceHost").value = configArr.srchost;
+  document.getElementById("multiplier-limit").value  = configArr.mulLim;
+  document.getElementById("multiplier-suspend").value= configArr.mulSus;
+  document.getElementById("threshold-limit").value   = configArr.thrLim;
+  document.getElementById("threshold-suspend").value = configArr.thrSus;
+  for(var i = 0; i < configArr.instances.length; i++) {
+    instances[configArr.instances[i].domain] = configArr.instances[i];
+    if(typeof configArr.instances[i].reasons == "object") {
+      reasonsArr = [];
+      for(j = 0; j < configArr.instances[i].reasons.length; j++) {
+        reasonsArr[configArr.instances[i].reasons[j].domain] = 
+          configArr.instances[i].reasons[j].text;
+      }
+      configArr.instances[i].reasons = reasonsArr;
+    }
+  }
+}
+
+// Creates a pool of threads checking if each domain is impotent
+// And setting the 'impotent' property accordingly
+// A host is impotent if it returns an error or a 404 trying to check its blocklist
+// This function is recursive, to act as a thread pool
+// You pass it a set of instances, it will call itself until the list is exhausted
+function checkImpotence(hostList, instance, response = null) {
+       // If this host already has the impotent property set, don't re-check it
+  if(typeof instances[instance].impotent != "undefined" && hostList.length > 0) {
+    instance = hostList.pop();
+    checkImpotence(hostList, instance, response);
+  }
+  // Set the impotence property based on the response from the last fetch
+  // And remove impotent doments from the list by setting display to none
+  if(response != null) {
+    if(response.status == "404") {
+      instances[instance].impotent = 1;
+      if(instances[instance].impotent == 1) {
+        document.getElementById("instance-"+instance).style.display = "none";
+      } else {
+        document.getElementById("instance-"+instance).style.display = "block";
+      }
+    } else if(response.status == "200") {
+      instances[instance].impotent = 0;
+      if(instances[instance].impotent == 1) {
+        document.getElementById("instance-"+instance).style.display = "none";
+      } else {
+        document.getElementById("instance-"+instance).style.display = "block";
+      }
+    } else {
+      instances[instance].impotent = 1;
+      if(instances[instance].impotent == 1) {
+        document.getElementById("instance-"+instance).style.display = "none";
+      } else {
+        document.getElementById("instance-"+instance).style.display = "block";
+      }
+    }
+  }
+
+       // If there's nothing left on the list, stop
+  if(hostList.length == 0) {
+    return;
+  }
+
+  // Fetch the block list for the next instance on the list
+  var instance = hostList.pop();
+  fetch("https://"+instance+"/api/v1/instance/domain_blocks", { signal: AbortSignal.timeout(5000) })
+    .then(response => checkImpotence(hostList, instance, response))
+    .catch(response => checkImpotence(hostList, instance, response));
+}
+
+
+/*
+ * MAIN ACTION FUNCTIONS
+ */
+async function synchronize() {
+  instances = [];
+
+  // Configure each list for default sort order
+  var lists = Array("hostsTrustedList", "hostsUntrustedList", "hostsKnownList",
+                    "hostseoeList", "hostsaoeList", "hostsfofList");
+  for(var i = 0; i < lists.length; i++) {
+    var listId = lists[i]; //.getAttribute("id");
+    sortFuncArr[listId] = {};
+    sortFuncArr[listId].direction = 1;
+    sortFuncArr[listId].function = function(a,b) {
+      return sortDomains(1,a,b);};
+  }
+
+  // Synchronize the known hosts list
+  var sourceInstance = "https://" + document.getElementById("sourceHost").value;
+  var f1 = fetch(sourceInstance + "/api/v1/instance/peers")
+    .then(response => response.json())
+    .then(peers => {
+      for(var i = 0; i < peers.length; i++) {
+        var instance = peers[i];
+
+        if( instances[instance] == null ) {
+          instances[instance] = new Object();
+          instances[instance].domain = instance;
+          instances[instance].reasons = [];
+                                       instances[instance].trust = 0;
+        }
+      }
+    });
+
+  // Synchronize the untrusted hosts list
+  var f2 = fetch(sourceInstance + "/api/v1/instance/domain_blocks")
+    .then(response => response.json())
+    .then(blocklist => {
+      console.log(blocklist);
+
+      for(var i = 0; i < blocklist.length; i++) {
+        instance = blocklist[i].domain;
+        if(instance.indexOf("*") != -1) {
+          continue;
+        }
+        if(instances[instance] == null) {
+          instances[instance] = new Object();
+          instances[instance].domain = instance;
+          instances[instance].reasons = [];
+        }
+
+        if( blocklist[i].comment == null ) {
+          blocklist[i].comment = "[ No comments provided ]"
+        }
+        if( blocklist[i].severity == "silence" ) {
+          instances[instance].trust =  document.getElementById("threshold-suspend").value;
+          instances[instance].reasons[instance] = "SILENCED: "+blocklist[i].comment;
+        } else {
+          instances[instance].trust = document.getElementById("threshold-limit").value;
+          instances[instance].reasons[instance] = "LIMITED: "+blocklist[i].comment;
+        }
+      }
+  });
+
+       // Re-render the lists once both lists are loaded (or failed)
+  Promise.allSettled([f1, f2])
+    .then(resetHosts);
+}
+
+async function process() {
+  document.getElementById("processButton").disabled = true;
+  var total = 0;
+  // Loop over all trusted instances and fetch the block list
+  // Multiply the source host's trust value by the multiplier and add to subTrust counter
+  for(instance in instances) {
+    if(instances[instance].trust > 0) {
+      try {
+        await fetch("https://"+instance+"/api/v1/instance/domain_blocks")
+          .then(response => response.json())
+          .then(blocklist => {
+            for(var i = 0; i < blocklist.length; i++) {
+              inst = blocklist[i].domain;
+              if(inst.indexOf("*") != -1) {
+                continue;
+              }
+              if(instances[inst] == null) {
+                instances[inst] = new Object();
+                instances[inst].domain = inst;
+                instances[inst].reasons = [];
+              }
+
+              if(typeof instances[inst].subtrust == "undefined") {
+                instances[inst].subtrust = 0;
+              }
+              if( blocklist[i].comment == null ) {
+                blocklist[i].comment = "[ No comments provided ]"
+              }
+              if( blocklist[i].severity == "silence" ) {
+                instances[inst].subtrust -= instances[instance].trust * 
+                    parseFloat(document.getElementById("multiplier-suspend").value);
+                instances[inst].reasons[instance] = "SILENCED: "+blocklist[i].comment;
+              } else {
+                instances[inst].subtrust -= instances[instance].trust * 
+                    parseFloat(document.getElementById("multiplier-limit").value);
+                instances[inst].reasons[instance] = "LIMITED: "+blocklist[i].comment;
+              }
+            }
+            total++;
+        });
+      } catch(e) {
+        console.log(e);
+      }
+    }
+  }
+
+  // Normalize trust values by dividing subTrust by the total number of lists checked
+  for(instance in instances) {
+    if(typeof instances[instance].subtrust != "undefined") {
+      instances[instance].trust = instances[instance].subtrust / total;
+      if(instances[instance].trust > 100) { instances[instance].trust = 100; }
+      if(instances[instance].trust < -100) { instances[instance].trust = -100; }
+    }
+  }
+  resetHosts();
+  document.getElementById("processButton").disabled = false;
+}
+
+// Generates the 'Associating with Enemies" list
+// Gets the instances of the 800 users that each trusted instance has
+//   communicated with most recently
+async function assocWithEnemies() {
+  document.getElementById("assocWithEnemies").disabled = true;
+  var total = 0;
+  subLists.aoe = [];
+  for(instance in instances) {
+    if(instances[instance].trust < 0) {
+      try {
+        var c = 0;
+        while(c < 10) {
+          await fetch("https://"+instance+"/api/v1/directory?limit=80&offset=0", {signal: AbortSignal.timeout(5000)})
+            .then(response => response.json())
+            .then(directory => {
+              if(directory.length < 80) {
+                c = 10;
+              }
+              for(var i = 0; i < directory.length; i++) {
+                inst = directory[i].acct.split("@")[1];
+                if(inst == null || inst.indexOf("*") != -1) {
+                  continue;
+                }
+                if(subLists.aoe[inst] == null) {
+                  subLists.aoe[inst] = new Object();
+                  subLists.aoe[inst].domain = inst;
+                  subLists.aoe[inst].reasons = [];
+                  subLists.aoe[inst].trust = 0;
+                  subLists.aoe[inst].aoesubtrust = 0;
+                }
+
+                subLists.aoe[inst].aoesubtrust += parseFloat(instances[instance].trust);
+                if(subLists.aoe[inst].aoesubtrust < total) {
+                  total = subLists.aoe[inst].aoesubtrust;
+                }
+              }
+          });
+          c++;
+        }
+      } catch(e) {
+        console.log(e);
+      }
+    }
+  }
+
+  // Normalize trust values
+  for(instance in subLists.aoe) {
+    if(typeof subLists.aoe[instance].aoesubtrust != "undefined") {
+      subLists.aoe[instance].trust = (subLists.aoe[instance].aoesubtrust * 100) / total;
+      if(subLists.aoe[instance].trust > 100) { subLists.aoe[instance].trust = 100; }
+      if(subLists.aoe[instance].trust < -100) { subLists.aoe[instance].trust = -100; }
+    }
+  }
+  addTable("aoe", "Associates of Enemies", subLists.aoe);
+  document.getElementById("assocWithEnemies").disabled = false;
+}
+
+// Generates the 'Associating with Friends' list
+// Gets the instances of the 800 users that each trusted instance has
+//   communicated with most recently.
+async function friendsOfFriends() {
+  document.getElementById("friendsOfFriends").disabled = true;
+  var total = 0;
+  subLists.fof = [];
+  for(instance in instances) {
+    if(instances[instance].trust > 0) {
+      try {
+        var c = 0;
+        while(c < 10) { 
+          await fetch("https://"+instance+"/api/v1/directory?limit=80&offset="+(80*c), 
+            {signal: AbortSignal.timeout(5000) }) 
+            .then(response => response.json())
+            .then(directory => {
+              if(directory.length < 80) {
+                c = 10;
+              }
+              for(var i = 0; i < directory.length; i++) {
+                inst = directory[i].acct.split("@")[1];
+                if(inst == null || inst.indexOf("*") != -1) {
+                  continue;
+                }
+                if(subLists.fof[inst] == null) {
+                  subLists.fof[inst] = new Object();
+                  subLists.fof[inst].domain = inst;
+                  subLists.fof[inst].reasons = [];
+                  subLists.fof[inst].trust = 0;
+                  subLists.fof[inst].fofsubtrust = 0;
+                }
+
+                subLists.fof[inst].fofsubtrust += parseFloat(instances[instance].trust);
+                if(subLists.fof[inst].fofsubtrust > total) {
+                  total = subLists.fof[inst].fofsubtrust;
+                }
+              }
+          });
+          c++;
+        }
+      } catch(e) {
+        console.log(e);
+      }
+    }
+  }
+  // Normalize trust values
+  for(instance in subLists.fof) {
+    if(typeof subLists.fof[instance].fofsubtrust != "undefined") {
+      subLists.fof[instance].trust = (int)(subLists.fof[instance].fofsubtrust * 100) / total;
+      if(subLists.fof[instance].trust > 100) { subLists.fof[instance].trust = 100; }
+      if(subLists.fof[instance].trust < -100) { subLists.fof[instance].trust = -100; }
+    }
+  }
+  addTable("fof", "Friends of Friends", subLists.fof);
+  document.getElementById("friendsOfFriends").disabled = false;
+}
+
+// Generates the 'Enemies of Enemies' list
+// Get the instances blocked by the untrusted instances
+async function enemiesOfEnemies() {
+  document.getElementById("enemiesOfEnemies").disabled = true;
+  var total = 0;
+  subLists.eoe = [];
+  for(instance in instances) {
+    if(instances[instance].trust < 0) {
+      try {
+        await fetch("https://"+instance+"/api/v1/instance/domain_blocks", { signal: AbortSignal.timeout(5000) })
+          .then(response => response.json())
+          .then(blocklist => {
+            for(var i = 0; i < blocklist.length; i++) {
+              inst = blocklist[i].domain;
+              if(inst.indexOf("*") != -1) {
+                continue;
+              }
+              if(subLists.eoe[inst] == null) {
+                subLists.eoe[inst] = new Object();
+                subLists.eoe[inst].domain = inst;
+                subLists.eoe[inst].reasons = [];
+                subLists.eoe[inst].eoesubtrust = 0;
+              }
+
+              if( blocklist[i].comment == null ) {
+                blocklist[i].comment = "[ No comments provided ]"
+              }
+              if( blocklist[i].severity == "silence" ) {
+                subLists.eoe[inst].eoesubtrust -= instances[instance].trust *
+                    parseFloat(document.getElementById("multiplier-suspend").value);
+                subLists.eoe[inst].reasons[instance] = "SILENCED: "+blocklist[i].comment;
+              } else {
+                subLists.eoe[inst].eoesubtrust -= instances[instance].trust *
+                    parseFloat(document.getElementById("multiplier-limit").value);
+                subLists.eoe[inst].reasons[instance] = "LIMITED: "+blocklist[i].comment;
+              }
+            }
+            total++;
+        });
+      } catch(e) {
+        console.log(e);
+      }
+    }
+  }
+  // Normalize trust values
+  for(instance in subLists.eoe) {
+    if(typeof subLists.eoe[instance].eoesubtrust != "undefined") {
+      subLists.eoe[instance].trust = subLists.eoe[instance].eoesubtrust / total;
+      if(subLists.eoe[instance].trust > 100) { subLists.eoe[instance].trust = 100; }
+      if(subLists.eoe[instance].trust < -100) { subLists.eoe[instance].trust = -100; }
+    }
+  }
+  addTable("eoe", "Enemies of Enemies", subLists.eoe);
+  document.getElementById("enemiesOfEnemies").disabled = false;
+}
+
+// (Re-)render a list to the page
+function addTable(id, title, data) {
+  var mainDiv = document.getElementById("hosts"+id);
+
+  // Additional tables can be added in any order/combination
+  // So we don't want to delete if they're already there
+  // Because then we'd have to remember the order...
+  // Instead we only delete the list itself
+       // (But this function also adds those additional tables
+       //  so it can't assume the table is there either)
+  if(mainDiv != null) {
+               var listElem = document.getElementById("hosts"+id+"List");
+  } else {
+    // Build the element if it didn't already exist
+               mainDiv = document.createElement("div");
+    mainDiv.setAttribute("id", "hosts"+id);
+    mainDiv.setAttribute("class", "subpanel");
+
+    var button = document.createElement("button");
+    button.setAttribute("id", "hosts"+id+"Open");
+    button.setAttribute("class", "panelButton");
+    button.innerHTML = "+";
+    button.onclick = openPanel;
+    mainDiv.appendChild(button);
+
+    var header = document.createElement("h2");
+    header.innerHTML = title;
+    mainDiv.appendChild(header);
+
+    var listElem = document.createElement("div");
+    listElem.setAttribute("id", "hosts"+id+"List");
+    listElem.setAttribute("class", "list");
+    listElem.setAttribute("listId", id);
+
+               mainDiv.appendChild(listElem);
+                       document.getElementById("hostsPanel").appendChild(mainDiv);
+       }
+  // Resets the list element
+  listElem.innerHTML =
+    "<div><h3 onclick='setSort(event);'>Instance</h3>" +
+    "<div class='comments'></div>"+
+    "<div onclick='setSort(event);'><h3>Trust Value</h3></div></div>";
+
+  // Note that this is where it is doing the actual sorting of the lists
+  // Then it creates an entry for each in the list
+       var keys = Object.keys(data).sort(sortFuncArr["hosts"+id+"List"].function);
+       for(var i = 0; i < keys.length; i++) {
+    var instance = keys[i];
+
+    var instElem = document.createElement("div");
+    instElem.innerHTML = "<h3>" + instance + "</h3>";
+    instElem.setAttribute("id", "instance-"+instance);
+    instElem.setAttribute("class", "instanceElem");
+
+    var commentsElem = document.createElement("div");
+    commentsElem.setAttribute("class", "comments");
+    commentsElem.innerHTML = "";
+    for(inst in data[instance].reasons) {
+      commentsElem.innerHTML += inst + " - " + data[instance].reasons[inst] + "<br>";
+    }
+    instElem.appendChild(commentsElem);
+
+    var divElem = document.createElement("div");
+    var rangeElem = document.createElement("input");
+    rangeElem.setAttribute("type", "range");
+    rangeElem.setAttribute("min",  "-100");
+    rangeElem.setAttribute("max",  "100");
+    rangeElem.setAttribute("value",data[instance].trust);
+    rangeElem.oninput = syncRange;
+    rangeElem.onchange = updateRange;
+    divElem.appendChild(rangeElem);
+
+    var valElem = document.createElement("input");
+    valElem.setAttribute("type", "text");
+    valElem.value = data[instance].trust;
+    valElem.onchange = updateVal;
+    divElem.appendChild(valElem);
+    instElem.appendChild(divElem);
+
+    // We only have the "Add" button on the additional lists
+               if(id != "Known" && id != "Trusted" && id != "Untrusted") {
+      addButton = document.createElement("button");
+      addButton.onclick = subListAdd;
+      addButton.innerHTML = "Add";
+      instElem.appendChild(addButton);
+    }
+
+    listElem.appendChild(instElem);
+  }
+}
+
+// Handles the 'Add' button on additional lists
+function subListAdd(event) {
+  var instance = event.target.parentElement.children[0].innerText;
+  var id = event.target.parentElement.parentElement.getAttribute("listId");
+
+  // TODO: Check for confirmation if it already exists
+  instances[instance] = subLists[id][instance];
+  event.target.parentElement.parentElement.removeChild(event.target.parentElement);
+  resetHosts();
+}
+
+// Handles clicking on the column header to sort
+function setSort(event) {
+  var value = event.target.innerText;
+  // The element clicked could be at two different depths...
+  // I should probably just make that consistent...
+  var list = event.target.parentElement.parentElement.getAttribute("id");
+  if(list == null) {
+    list = event.target.parentElement.parentElement.parentElement.getAttribute("id");
+  }
+       // If they clicked 'Instance', sort by domain
+  if( value == 'Instance' ) {
+    sortFuncArr[list].direction = sortFuncArr[list].direction * -1;
+    sortFuncArr[list].function = function(a,b) {
+      return sortDomains(sortFuncArr[list].direction,a,b);};
+       // If they clicked 'Trust Value', sort by that
+  } else if( value == 'Trust Value' ) {
+    sortFuncArr[list].direction = sortFuncArr[list].direction * -1;
+               // Main three lists are in 'instances', others are in subLists
+               if( list == "hostsKnownList" || list == "hostsTrustedList" || list == "hostsUntrustedList" ) {
+      sortFuncArr[list].function = function(a,b) {
+                               return sortTrust(sortFuncArr[list].direction,instances,a,b);};
+               } else {
+                       // list will be an element id, like 'hostseoeList', so slice takes off 'hosts' and 'List'
+                       sortFuncArr[list].function = function(a,b) {
+                               return sortTrust(sortFuncArr[list].direction,subLists[list.slice(5).slice(0,-4)],a,b);};
+    }
+  }
+  resetHosts();
+}
+
+// Trigger a re-render of all lists
+function resetHosts() {
+  var trusted = [];
+  var untrusted = [];
+  var known = [];
+  for(instance in instances) {
+               if(instances[instance].trust > 0) {
+                       trusted[instance] = instances[instance];
+               } else if(instances[instance].trust < 0) {
+                       untrusted[instance] = instances[instance];
+    } else {
+                       known[instance] = instances[instance];
+    }
+  }
+       addTable("Trusted", "Trusted Hosts", trusted);
+  addTable("Untrusted", "Untrusted Hosts", untrusted);
+       addTable("Known", "Known Hosts", known);
+       var lists = Object.keys(subLists);
+       for(var i = 0; i < lists.length; i++) {
+               list = lists[i];
+               addTable(list, list, subLists[list]);
+       }
+}
+
+// Toggle a panel open or closed by showing or hiding the list element
+function openPanel(event) {
+  var listElem = this.parentElement.getElementsByClassName("list")[0];
+  if( listElem.style.display == "block" ) {
+    this.parentElement.getElementsByClassName("list")[0].style.display = "none";
+    this.innerHTML = "+";
+  } else {
+    this.parentElement.getElementsByClassName("list")[0].style.display = "block";
+    this.innerHTML = "-";
+  }
+}
+
+// Update the instance's range slider from a value entered in the text box
+// (Also update the element's trust value, and move to the appropriate list if necessary)
+function updateRange(event) {
+  var id = this.parentElement.parentElement.parentElement.getAttribute("listId");
+  var instance = this.parentElement.parentElement.firstChild.innerText;
+  if( id == null) {
+    instances[instance].trust = this.value;
+  } else {
+    subLists[id][instance].trust = this.value;
+  }
+  this.parentElement.children[1].value = this.value;
+
+  if(id == null) {
+    var parentPanel = this.parentElement.parentElement.parentElement;
+    var instElem = this.parentElement.parentElement;
+    if( this.value < 0 && parentPanel != document.getElementById("hostsUntrustedList") ) {
+      parentPanel.removeChild(instElem);
+      document.getElementById("hostsUntrustedList").appendChild(instElem);
+    } else if( this.value > 0 && parentPanel != document.getElementById("hostsTrustedList") ) {
+      parentPanel.removeChild(instElem);
+      document.getElementById("hostsTrustedList").appendChild(instElem);
+    } else if( this.value == 0 && parentPanel != document.getElementById("hostsKnownList") ) {
+      parentPanel.removeChild(instElem);
+      document.getElementById("hostsKnownList").appendChild(instElem);
+    }
+  }
+}
+
+// Update the instance's text box each time the range slider is moved
+function syncRange(event) {
+  this.parentElement.children[1].value = this.value;
+}
+
+// Update the instance's text box from a value entered in the range slider
+// (Also update the element's trust value, and move to the appropriate list if necessary)
+function updateVal(event) {
+  var id = event.target.parentElement.parentElement.getAttribute("listId");
+  var instance = this.parentElement.parentElement.firstChild.innerText;
+  if(this.value < -100) { this.value = -100; }
+  if(this.value > 100) { this.value = 100; }
+  this.parentElement.children[0].value = this.value;
+  instances[instance].trust = this.value;
+
+  // The initial three lists don't have this id element
+       // If it's not on those lists it shouldn't move when the trust value is changed
+  if(id == null) {
+    var parentPanel = this.parentElement.parentElement.parentElement;
+    var instElem = this.parentElement.parentElement;
+    if( this.value < 0 && parentPanel != document.getElementById("hostsUntrustedList") ) {
+      parentPanel.removeChild(instElem);
+      document.getElementById("hostsUntrustedList").appendChild(instElem);
+    } else if( this.value > 0 && parentPanel != document.getElementById("hostsTrustedList") ) {
+      parentPanel.removeChild(instElem);
+      document.getElementById("hostsTrustedList").appendChild(instElem);
+    } else if( this.value == 0 && parentPanel != document.getElementById("hostsKnownList") ) {
+      parentPanel.removeChild(instElem);
+      document.getElementById("hostsKnownList").appendChild(instElem);
+    }
+  }
+}
+
+// Draw the pop-up to import or export from a string
+function importStr(str = "") {
+  var shade = document.createElement("div");
+  shade.setAttribute("id", "shade");
+  shade.onclick = function() {
+    document.body.removeChild(document.getElementById("shade"));
+    document.body.removeChild(document.getElementById("shade-text"));
+    document.body.removeChild(document.getElementById("shade-text-button"));
+  }
+
+  var text  = document.createElement("textarea");
+  text.innerHTML = str;
+  text.setAttribute("id", "shade-text");
+
+  var btn = document.createElement("button");
+  btn.innerHTML = "Import";
+  btn.setAttribute("id", "shade-text-button");
+  btn.onclick = function() {
+    loadStr(document.getElementById("shade-text").value);
+    document.getElementById("shade").onclick();
+    resetHosts();
+  };
+
+  document.body.appendChild(shade);
+  document.body.appendChild(text);
+  document.body.appendChild(btn);
+}
+
+// Leverage the import code to display an export string
+function exportStr() {
+  importStr(saveStr());
+}
+
+// Load from the browser cache
+function loadCache() {
+  loadStr(localStorage.getItem("config"));
+  resetHosts();        
+}
+
+// Save to the browser cache
+function saveCache() {
+  localStorage.setItem("config", saveStr());
+}
+
+// Start ten threads checking and hiding impotent domains
+function hideImpotent() {
+  var keys = Object.keys(instances);
+  for(i = 0; i < keys.length; i+=(keys.length/10)) {
+    var sublist = keys.slice(i, i+(keys.length/10));
+    var instance = sublist.pop();
+    checkImpotence( sublist, instance )
+  }
+}
+
+// Hide any instances that are a subdomain of another known instance
+async function hideSubdomains() {
+       for(instance in instances) {
+               var domainParts = instance.split('.');
+               for(var i = 1; i < domainParts.length-1; i++) {
+                       var parent = domainParts.slice(i).join('.');
+                       if(typeof instances[parent] != undefined) {
+                               instances[instance].parent = parent;
+                               var instElem = document.getElementById("instance-"+instance);
+                               if(instElem != null) {
+                                       instElem.style.display = "none";
+                               }
+                               break;
+                       }
+               }
+  }
+}
+
+// Export a CSV to be used with the given import script
+function exportCSV() {
+  var data = "";
+  for(instance in instances) {
+    var action = "";
+    if(instances[instance].trust <= document.getElementById('threshold-limit').value) {
+      action = "limit";
+    } else if(instances[instance].trust <= document.getElementById('threshold-suspend').value) {
+      action = "suspend";
+    }
+
+    if(action != "") {
+      data += instance + ",";
+      data += action + ",";
+      data += '"';
+      for(inst in instances[instance].reasons) {
+        data += inst + " - " + instances[instance].reasons[inst] + "\n";
+      }
+      data = data.trim();
+      data += '"\n';
+    }
+  }
+  var elem = document.createElement("a");
+  elem.setAttribute("href", "data:text/plain;charset=utf-8," 
+    + encodeURIComponent(data.trim()));
+  elem.setAttribute('download', 'mastowot.csv');
+  document.body.appendChild(elem);
+  elem.click();
+  document.body.removeChild(elem);
+}
+
+// @license-end