Created
August 1, 2023 00:22
-
-
Save stefanbohacek/4dde5b0231e504a4ae29e234306b8602 to your computer and use it in GitHub Desktop.
Source code for the "Preview domain blocks before moving to a new Mastodon instance" blog post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Document</title> | |
<link | |
rel="stylesheet" | |
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" | |
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" | |
crossorigin="anonymous" | |
/> | |
</head> | |
<body> | |
<div class="bg-light p-5 mt-5 mb-5"> | |
<form id="data-form"> | |
<div class="mb-3"> | |
<label for="instance" class="form-label">Mastodon instance</label> | |
<div class="input-group"> | |
<span class="input-group-text" id="basic-addon3">https://</span> | |
<input | |
required | |
type="text" | |
class="form-control" | |
name="instance" | |
id="instance" | |
placeholder="mastodon.social" | |
/> | |
</div> | |
<div class="form-text"> | |
The name of the Mastodon instance, without <code>https</code>. | |
</div> | |
</div> | |
<div class="mb-3"> | |
<label for="data" class="form-label" | |
>Fediverse connections data</label | |
> | |
<input | |
required | |
class="form-control" | |
type="file" | |
name="data" | |
id="data" | |
accept=".csv" | |
/> | |
<div class="form-text"> | |
Download yours | |
<a | |
href="https://data.stefanbohacek.dev/projects/fediverse" | |
target="_blank" | |
>here</a | |
>. | |
</div> | |
</div> | |
<button id="form-submit" type="submit" class="btn btn-primary"> | |
Submit | |
</button> | |
</form> | |
<div id="results"></div> | |
</div> | |
<script> | |
const csvToJSON = (csv, headers, quoteChar = '"', delimiter = ",") => { | |
const regex = new RegExp( | |
`\\s*(${quoteChar})?(.*?)\\1\\s*(?:${delimiter}|$)`, | |
"gs" | |
); | |
const match = (line) => | |
[...line.matchAll(regex)].map((m) => m[2]).slice(0, -1); | |
const lines = csv.split("\n"); | |
const heads = headers ?? match(lines.shift()); | |
return lines.map((line) => { | |
return match(line).reduce((acc, cur, i) => { | |
const val = cur.length <= 0 ? null : Number(cur) || cur; | |
const key = heads[i] ?? `extra_${i}`; | |
return { ...acc, [key]: val }; | |
}, {}); | |
}); | |
}; | |
const getDomainBlocks = async (instance) => { | |
let results = {}; | |
try { | |
const resp = await fetch( | |
`https://${instance}/api/v1/instance/domain_blocks` | |
); | |
const respJSON = await resp.json(); | |
respJSON.forEach((domain) => { | |
if (domain.severity === "suspend") { | |
results[domain.domain] = domain; | |
} | |
}); | |
} catch (error) { | |
alert(`Domain block list not available for ${instance}.`); | |
toggleFormElements(form, false); | |
} finally { | |
return results; | |
} | |
}; | |
const toggleFormElements = (form, disabled) => { | |
const formElements = form.elements; | |
for (element of formElements) { | |
if (element.tagName === "BUTTON") { | |
element.disabled = disabled; | |
} else { | |
element.readOnly = disabled; | |
} | |
} | |
}; | |
const form = document.getElementById("data-form"); | |
const formSubmitBtn = document.getElementById("form-submit"); | |
const resultsElement = document.getElementById("results"); | |
form.addEventListener("submit", (event) => { | |
toggleFormElements(form, true); | |
event.preventDefault(); | |
const formData = new FormData(form); | |
const values = [...formData.entries()]; | |
const instance = formData.get("instance"); | |
const data = formData.get("data"); | |
let lostContacts = []; | |
let reader = new FileReader(); | |
reader.onload = async (e) => { | |
const dataJSON = csvToJSON(e.target.result); | |
const blockedDomains = await getDomainBlocks(instance); | |
let resultsHTML = ""; | |
if (blockedDomains) { | |
const blockedDomainNames = Object.keys(blockedDomains); | |
if (blockedDomainNames && blockedDomainNames.length) { | |
for (const instance of dataJSON) { | |
if (blockedDomainNames.includes(instance.domain)) { | |
instance.comment = blockedDomains[instance.domain].comment; | |
lostContacts.push(instance); | |
} | |
} | |
if (lostContacts && lostContacts.length) { | |
resultsHTML = ` | |
<p class="mt-4">By moving to ${instance}, you will lose ${lostContacts.length.toLocaleString()} connection(s).</p> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th scope="col">Instance</th> | |
<th scope="col">Connections</th> | |
<th scope="col">Comment</th> | |
</tr> | |
</thead> | |
<tbody> | |
${lostContacts | |
.map( | |
(domain) => ` | |
<tr> | |
<th scope="row">${domain.domain}</th> | |
<td>${domain.connections.toLocaleString()}</td> | |
<td>${domain.comment}</td> | |
</tr> | |
` | |
) | |
.join("")} | |
</tbody> | |
</table> | |
`; | |
} else { | |
resultsHTML = `<p class="mt-4"><strong>Good news!</strong> None of your contacts' instances are blocked by ${instance}.`; | |
} | |
} | |
} | |
resultsElement.innerHTML = resultsHTML; | |
if (lostContacts && lostContacts.length){ | |
resultsElement.scrollIntoView({ | |
behavior: 'smooth' | |
}); | |
} | |
toggleFormElements(form, false); | |
}; | |
reader.readAsBinaryString(data); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment