From 4e6170e73ff253efc6efdd661fd462cee37c6686 Mon Sep 17 00:00:00 2001 From: Brian Flowers Date: Tue, 17 Jan 2023 22:14:14 -0500 Subject: [PATCH 1/1] Initial commit --- index.html | 1043 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ mastowot.css | 181 ++++++++++ mastowot.html | 227 +++++++++++++ mastowot.js | 776 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 2227 insertions(+) create mode 100644 index.html create mode 100644 mastowot.css create mode 100644 mastowot.html create mode 100644 mastowot.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..f914fb4 --- /dev/null +++ b/index.html @@ -0,0 +1,1043 @@ + + + MastoWoT? + + + + +

MastoWoT: A Web-of-Trust inspired defederation station

+ + +
+ +

Instructions

+
+

This tool is intended for Mastodon instance administrators

+

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 :)

+

This tool is designed to suggest 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.

+

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".

+

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.

+

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.

+

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.

+

Also please be aware that some instances may choose to obfuscate certain + URLs from their block list. Please don't do that. This tool cannot + work with obfuscated URLs.

+

The remaining options are mentioned below:

+
    +
  • 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.
  • +
  • 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.
  • +
  • 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.
  • +
  • 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.
  • +
  • Load from cache: Load instances list from the browser cache
  • +
  • Save to cache: Save instances list to your browser cache
  • +
  • Export to you: Show the instances list as a JSON string
  • +
  • Import from you: Import a JSON string exported previously
  • +
+ +
+
+# 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
+
+
+
+
+TODO:
+  Styling:
+    Color chooser, high contrast mode
+  Sort lists by name or trust level
+  Determine if blocked hosts have/had direct associations? and when?
+
+
+

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 :)

+
+
+
+
+ +

Trusted Hosts

+
+
+

Instance

+
+

Trust Value

+
+
+
+
+ +
+ +

Untrusted Hosts

+
+
+

Instance

+
+

Trust Value

+
+
+
+
+ +
+ +

Known Hosts

+
+
+

Instance

+
+

Trust Value

+
+
+
+
+
+ +
+ + +
+ +
+ + + + +
+ + +
+ + +
+ +
+ + +
+

Multipliers

+ + + + +
+
+

Thresholds

+ + + + +
+
+ + + diff --git a/mastowot.css b/mastowot.css new file mode 100644 index 0000000..734bba4 --- /dev/null +++ b/mastowot.css @@ -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 index 0000000..810dd19 --- /dev/null +++ b/mastowot.html @@ -0,0 +1,227 @@ + + + MastoWoT + + + + + +

MastoWoT: A Web-of-Trust inspired defederation station

+ + + +
+ +

Instructions

+
+

This tool is intended for Mastodon instance administrators

+

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 :)

+

This tool is designed to suggest 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.

+

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".

+

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.

+

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.

+

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.

+

Also please be aware that some instances may choose to obfuscate certain + URLs from their block list. Please don't do that. This tool cannot + work with obfuscated URLs.

+

The remaining options are mentioned below:

+
    +
  • 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.
  • +
  • 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.
  • +
  • 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.
  • +
  • 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.)
  • +
  • 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.)
  • +
  • Load from cache: Load instances list from the browser cache
  • +
  • Save to cache: Save instances list to your browser cache
  • +
  • Export to you: Show the instances list as a JSON string
  • +
  • Import from you: Import a JSON string exported previously
  • +
+ +
+
+# 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
+
+
+
+
+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...)
+
+

[ 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 :) ]

+

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.

+
+
+
+
+ +

Trusted Hosts

+
+
+

Instance

+
+

Trust Value

+
+
+
+
+ +
+ +

Untrusted Hosts

+
+
+

Instance

+
+

Trust Value

+
+
+
+
+ +
+ +

Known Hosts

+
+
+

Instance

+
+

Trust Value

+
+
+
+
+
+ +
+ + +
+ +
+ + + + + +
+ + +
+ + +
+ +
+ + +
+

Multipliers

+ + + + +
+
+

Thresholds

+ + + + +
+
+ + + diff --git a/mastowot.js b/mastowot.js new file mode 100644 index 0000000..71b9526 --- /dev/null +++ b/mastowot.js @@ -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 = + "

Instance

" + + "
"+ + "

Trust Value

"; + + // 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 = "

" + instance + "

"; + 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] + "
"; + } + 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 -- 1.8.3.1