initial commit
Todas las comprobaciones han sido exitosas
continuous-integration/drone Build is passing

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-07-18 22:19:35 +02:00
commit bfaab359eb
Se han modificado 46 ficheros con 10305 adiciones y 0 borrados

405
front/src/App.css Archivo normal
Ver fichero

@@ -0,0 +1,405 @@
:root[data-theme="dark"] {
--background-color: #212529;
--color: #fefefe;
--instance-list: #333333;
--footer: #cccccc;
}
:root[data-theme="light"] {
--background-color: #f5f5f5;
--color: #333333;
--instance-list: #f0f0f0;
--footer: #666666;
}
body {
background-color: var(--background-color);
text-align: center;
font-family: Verdana, Geneva, Tahoma, sans-serif;
scrollbar-width: thin;
}
h1,
h3,
h4,
.scan {
margin: 0 auto;
color: var(--color);
text-align: center;
}
h3 {
padding: 10px 0;
border-bottom: 1px solid var(--footer);
margin-bottom: 1rem;
}
a,
a:hover {
color: var(--color);
text-decoration: none;
}
input[type="text"] {
padding: 10px;
border: none;
border-bottom: 1px solid var(--footer);
background-color: var(--instance-list);
color: var(--color);
}
.placeholder {
font-size: small;
margin: 0 auto;
color: var(--color);
}
.title, .placeholder a, footer a, .download-csv {
cursor: pointer;
}
hr {
margin: 1rem auto;
border-bottom: 1px solid var(--footer);
width: 50%;
}
.tooltip {
visibility: hidden;
font-size: small;
text-align: left;
width: 2.2in;
background-color: var(--background-color);
color: var(--color);
border-radius: 5px;
padding: 1rem 1rem 0 0;
position: absolute;
z-index: 1;
opacity: 0;
transition: 0.5s;
border: 1px solid var(--footer);
}
.count:hover .tooltip {
visibility: visible;
opacity: 1;
}
.reverse {
margin: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--footer);
background-color: var(--color);
color: var(--background-color);
}
.reverse:hover {
color: var(--color);
background-color: var(--background-color);
cursor: pointer;
}
.instancelist {
list-style-type: none;
margin: 20px 0;
padding: 0;
color: var(--color);
max-height: 58vh;
overflow: auto;
scrollbar-width: thin;
}
@media (min-width: 992px) {
.instancelist li {
width: 30%;
}
.instancelist li:hover {
width: 35%;
}
.instance,
.placeholder,
h4 {
width: 30%;
}
.modal-content {
width: 28%;
}
}
@media (min-width: 600px) and (max-width: 992px) {
.instancelist li {
width: 60%;
}
.instancelist li:hover {
width: 65%;
}
.instance,
.placeholder,
h4 {
width: 60%;
}
.modal-content {
width: 58%;
}
}
@media (max-width: 600px) {
.instancelist li {
width: 90%;
}
.instancelist li:hover {
width: 95%;
}
.instance,
.placeholder,
h4 {
width: 90%;
}
.api {
display: none;
}
.modal-content {
width: 88%;
}
}
.instancelist li {
color: var(--color);
padding: 10px;
border-bottom: 1px solid var(--footer);
margin: 0 auto;
max-height: 1.5em;
overflow: hidden;
min-height: 1.5em;
line-height: 1.5;
}
.instancelist li:hover {
background-color: var(--instance-list);
cursor: pointer;
max-height: fit-content;
}
.instancelist li img {
width: 70%;
margin: 0 auto;
}
@keyframes opacity {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
footer {
text-align: center;
font-size: 12px;
padding: 10px;
color: var(--footer);
}
.count {
text-decoration: underline dotted var(--color);
cursor: help;
position: relative;
display: inline-block;
text-underline-offset: 2px;
}
.blocklist {
width: 90%;
text-align: left;
list-style: none;
padding: 1rem;
}
.blocklist li {
color: var(--color);
white-space: nowrap;
word-wrap: break-word;
overflow: hidden;
}
.blocklist li:hover {
white-space: inherit;
cursor: pointer;
color: var(--color);
}
.blocklist li a,
.blocklist li a:hover,
.blocklist li a:visited {
color: var(--color);
text-decoration: underline;
}
.blockinstance a,
.blockinstance a:hover {
color: var(--color);
text-decoration: underline;
cursor: help;
}
.blockcount {
font-weight: bolder;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
scrollbar-width: thin;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
position: relative;
background-color: var(--background-color);
margin: 10px auto;
padding: 0;
border: 1px solid var(--footer);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s;
}
.modal-header {
padding: 6px;
background-color: var(--color);
color: var(--background-color);
}
.modal-body {
color: var(--color);
background-color: var(--background-color);
padding: 6px;
}
.modal-footer {
padding: 6px;
background-color: var(--color);
color: var(--background-color);
}
.closemodal,
.download,
.capture {
color: var(--footer);
float: right;
font-size: 3rem;
font-weight: bolder;
margin: 0px 15px;
}
.closemodal:hover,
.closemodal:focus,
.download:hover,
.download:focus,
.capture:hover,
.capture:focus {
color: var(--color);
text-decoration: none;
cursor: pointer;
}
@keyframes animatetop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
@keyframes animatebottom {
from {
top: 0;
opacity: 1;
}
to {
top: -300px;
opacity: 0
}
}
h4 {
white-space: nowrap;
overflow: hidden;
position: relative;
border-left: 1px solid var(--background-color);
border-right: 1px solid var(--background-color);
transition: 0.2s;
}
h4 p {
margin: 0.3rem 0;
}
h4:hover {
width: 90%;
}
.bounce {
display: inline-block;
animation: marquee 90s linear infinite;
}
.bounce:hover {
animation-play-state: paused;
}
@keyframes marquee {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-100%);
}
}
.loader-content {
z-index: 0;
background-color: rgba(0, 0, 0, 0.4);
width: 100%;
height: 100%;
display: none;
position: fixed;
top: 0;
left: 0;
overflow: auto;
scrollbar-width: thin;
}
.loader {
background-color: var(--color);
width: fit-content;
margin: 50vh auto;
z-index: 1;
position: relative;
color: var(--background-color);
}

28
front/src/App.js Archivo normal
Ver fichero

@@ -0,0 +1,28 @@
import { useState } from 'react';
import './App.css';
import Title from './component/Title';
import Bar from './component/Bar';
import Count from './component/Count';
import Form from './component/Form';
import Scan from './component/Scan';
import Footer from './component/Footer';
import Loader from './component/Loader';
function App() {
const [searchTerm, setSearchTerm] = useState(''),
[matrix, setMatrix] = useState('off')
return (
<>
<Title />
<Bar setSearch={d => setSearchTerm(d)} />
<Count />
<Form searchTerm={searchTerm} matrix={matrix} />
<Scan />
<hr />
<Footer setCurrentMatrix={m => setMatrix(m)} />
<Loader />
</>
);
}
export default App;

8
front/src/App.test.js Archivo normal
Ver fichero

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

29
front/src/component/Bar.js Archivo normal
Ver fichero

@@ -0,0 +1,29 @@
import { useEffect, useState, useCallback } from 'react';
import dayjs from '../../node_modules/dayjs/';
import relativeTime from '../../node_modules/dayjs/plugin/relativeTime';
dayjs.extend(relativeTime)
const Bar = (prop) => {
const [bounce, setBounce] = useState([]),
fillBounce = useCallback(async () => {
try {
const response = await fetch('/api/outbox'),
result = await response.json()
if (result && result.length > 0) {
setBounce(result)
}
} catch (e) {
console.error(e)
}
})
useEffect(() => {
fillBounce()
}, [])
return (
<h4>
<p className="bounce">{bounce && bounce.length > 0 ? bounce.map(b => <a onClick={() => prop.setSearch(b.content.replace(/.*#/g, '').replace(/\<\/a\>\<\/p\>/, ''))}>{b.content.replace(/<[^>]*>/g, '').replace(new RegExp(new URL(window.location.href).host), '')} - {dayjs().to(b.published)}</a>).reduce((prev, curr) => [prev, ' | ', curr]) : []}</p>
</h4>
)
}
export default Bar;

41
front/src/component/Count.js Archivo normal
Ver fichero

@@ -0,0 +1,41 @@
import { useEffect, useState, useCallback } from 'react';
const Count = () => {
const [count, setCount] = useState('0'),
[statsres, setStatsres] = useState(''),
fillStats = useCallback(async () => {
try {
const [res, stats] = await Promise.all([(await fetch('/api/count')).json(), (await fetch('/api/stats')).json()])
setCount(res.count)
setStatsres(stats)
} catch (e) {
console.error(e)
}
})
useEffect(() => {
(async () => {
await fillStats()
})()
}, [])
return (
<h3>Search in <a href="/api/stats" target="_blank">
<span className="count">{count}<div className="tooltip">
<u><strong><center>STATS</center></strong></u>
<ul><li>Statuses AVG: {Math.round(statsres.status_avg)}</li>
<li>Statuses MAX: {statsres.status_max}</li>
<li>Domain AVG: {Math.round(statsres.domain_avg)}</li>
<li>Domain MAX: {statsres.domain_max}</li>
<li>Users AVG: {Math.round(statsres.user_avg)}</li>
<li>Users MAX: {statsres.user_max}</li>
<li>Stats Instances: {statsres.stats_filtered}</li>
<li>Total Instances: {statsres.instance_count}</li>
<li>Users by Instance: {(Math.round(statsres.user_avg) / statsres.instance_count).toFixed(2)}</li>
<li>Statuses by Domain: {(Math.round(statsres.status_avg) / Math.round(statsres.domain_avg)).toFixed(2)}</li>
<li>Statuses by User: {(Math.round(statsres.status_avg) / Math.round(statsres.user_avg)).toFixed(2)}</li>
</ul></div></span>
</a> public instances<span className="api"> - <a href="/api-docs" target="_blank">API docs</a></span>
</h3>
)
}
export default Count;

67
front/src/component/Footer.js Archivo normal
Ver fichero

@@ -0,0 +1,67 @@
import { useEffect, useState, useCallback } from 'react';
import Messenger from '../random-text';
const Footer = (prop) => {
const [theme, setTheme] = useState('dark'),
[matrix, setMatrix] = useState('off'),
[served, setServed] = useState({ served: 0, lastscan: 0, server: 0, instances: 0, peers: 0, created: 0, updated: 0 }),
loading = () => {
document.querySelector('.loader-content').style.display = 'initial'
setTimeout(() => {
document.querySelector('.loader-content').style.display = 'none'
}, 60 * 1000)
},
toggleTheme = useCallback(() => {
let tm = document.documentElement.dataset.theme
if (!theme || tm === 'light') {
setTheme('dark')
} else {
setTheme('light')
}
}),
refreshServed = useCallback(async () => {
try {
const response = await fetch('/api/served')
if (response.ok) {
setServed(await response.json())
}
} catch (e) {
console.error(e)
}
}),
toggleMatrix = useCallback(() => {
if (matrix === 'off') {
setMatrix('on')
prop.setCurrentMatrix('on')
} else {
setMatrix('off')
prop.setCurrentMatrix('off')
}
})
useEffect(() => {
setTheme(theme)
document.documentElement.dataset.theme = theme
refreshServed()
if (matrix === 'on') {
var walker = document.createTreeWalker(document.getElementById('root'), NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
}
}
}
}, [theme])
return (
<footer>Served <span className="served">{served.served}</span> times - Last scan <span className="lastscan">{served.lastscan}</span> peers of <span
className="server">{served.server}</span><br />
Total scanned <span id="instances">{served.instances}</span> instances with <span className="peers">{served.peers}</span> peers - <span
className="created">{served.created}</span> created - <span className="updated">{served.updated}</span> updated<br />
matrix <a className="matrix" onClick={() => toggleMatrix()}>{matrix}</a> - download json <a className="download_index" href="/api/download_index"
onClick={() => loading()} download="fediblock-index.json.gz" target="_blank">index</a> -
by <a href="https://about.manalejandro.com" target="_blank">ale</a> <s>&copy;</s>2025&nbsp;
<a className="darklight" onClick={() => toggleTheme()}>{!theme || theme === 'dark' ? '☼' : '☽'}</a>
</footer>
)
}
export default Footer;

167
front/src/component/Form.js Archivo normal
Ver fichero

@@ -0,0 +1,167 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import Modal from './Modal';
import Messenger from '../random-text';
const Form = (prop) => {
let csv
const [searchTerm, setSearchTerm] = useState(''),
[list, setList] = useState([]),
[typeList, setTypeList] = useState('ranking'),
[domain, setDomain] = useState({ domain: '' }),
[reverse, setReverse] = useState(false),
refList = useRef(null),
refDownload = useRef(null),
loading = () => {
document.querySelector('.loader-content').style.display = 'initial'
},
download = () => {
if (csv.split('\n').length > 2) {
refDownload.current.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
refDownload.current.download = 'fediblock-top100.csv'
}
},
listinstance = useCallback(async content => {
loading()
if (content && content.length > 0) {
try {
const result = await fetch('/api/list/' + content),
res = await result.json()
if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) {
setTypeList('list')
setList(res)
document.querySelector('.loader-content').style.display = 'none'
} else {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: No response')
}
} catch (e) {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: ' + e.message)
}
} else {
document.querySelector('.loader-content').style.display = 'none'
}
}),
ranking = useCallback(async () => {
loading()
try {
const result = await fetch('/api/ranking'),
res = await result.json()
if (Array.isArray(res) && res.length > 0) {
setTypeList('ranking')
setList(res)
document.querySelector('.loader-content').style.display = 'none'
} else {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: No response')
}
} catch (e) {
document.querySelector('.loader-content').style.display = 'none'
window.alert('Error: ' + e.message)
}
}),
filterKeys = useCallback(event => {
if (event.key && !event.ctrlKey && !event.altKey) {
if ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && event.target.value !== '')) {
if (event.key === '.' && event.target.value === '') {
return
} else if (event.key === 'Backspace' && event.target.value !== '') {
setSearchTerm(searchTerm.substring(0, searchTerm.length - 1))
} else {
setSearchTerm(searchTerm + event.key)
}
}
}
})
useEffect(() => {
if (searchTerm && searchTerm.length > 0) {
listinstance(searchTerm)
window.location.hash = searchTerm
} else {
(async () => {
await ranking()
})()
}
if (prop.matrix === 'on') {
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
}
}
}
}, [searchTerm, prop.matrix])
useEffect(() => {
if (prop.searchTerm && prop.searchTerm.length > 0) {
setSearchTerm(prop.searchTerm)
}
}, [prop.searchTerm])
useEffect(() => {
if (window.location.hash && window.location.hash !== '#') {
setSearchTerm(window.location.hash.substring(1))
}
}, [])
return (
<>
<section>
<input
autoFocus
type="text"
autoComplete="off"
className="instance"
placeholder="Type the name of the instance"
onKeyUp={(e) => filterKeys(e)}
value={searchTerm} />
<button className="reverse" title="Reverse search..." onClick={() => {
if (searchTerm && searchTerm.length > 0) {
setReverse(true)
setDomain({ domain: searchTerm })
loading()
}
}}>Reverse</button>
<div className="placeholder">{typeList === 'list' ? list.suggests && list.suggests.length > 0 ? list.suggests.map(suggest => (<a onClick={() => setSearchTerm(suggest)}>{suggest}</a>)).reduce((prev, curr) => [prev, ', ', curr]) : [] : []}</div>
<ul className="instancelist" ref={refList}>{typeList === 'list' ? list.instances.map(r =>
(<li><a onClick={() => {
if (r.blocks) {
setReverse(false)
setDomain({ domain: r.domain })
loading()
} else {
var a = document.createElement('a')
a.href = '/api/detail_api/' + r.domain
a.title = 'API info for ' + r.domain
a.target = '_blank'
a.dispatchEvent(new MouseEvent('click'))
}
}}>{r.domain}</a>
<span dangerouslySetInnerHTML={{ __html: r.blocks ? ` - ` + r.blocks + ` blocks&nbsp;` : `&nbsp;` }}></span>
<span dangerouslySetInnerHTML={{
__html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">&#9432;</a>` + `<br />` : `<br />`
}}></span>
<span dangerouslySetInnerHTML={{
__html: r.api?.title ? `<br /><br />` + r.api.title + ` - ` + r.api.uri + `<br />` : `<br /><br />`
}}></span>
<span dangerouslySetInnerHTML={{ __html: r.last ? `Last update: ` + (new Date(r.last)).toLocaleString() + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.email ? `Email: ` + r.api.email + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: `Registration: ` + (r.api?.registrations ? `open` : `closed`) + ` - Version: ` + r.api?.version + `<br />` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.stats ? `Users: ` + r.api.stats.user_count + ` - Statuses: ` + r.api.stats.status_count + ` - Domains: ` + r.api.stats.domain_count + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.description ? `Description: ` + r.api.description + `<br />` : `` }}></span>
<span dangerouslySetInnerHTML={{ __html: r.api?.thumbnail ? `<img domain="${r.domain}" loading="lazy" src="${r.api.thumbnail}" />` + `<br />` : `` }}></span>
</li>
)) : typeList === 'ranking' ? list.map((r, i) => {
if (i === 0) {
csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
return (<><li><strong>Top 100</strong><br /><small className="download-top"><a ref={refDownload} onClick={() => download()}>(Import CSV)</a></small></li><li>{i + 1} - {r.domain} - {r.count} blocks</li></>)
} else {
csv += !r.domain.match(/\*/) ? r.domain + ',suspend,False,False,"suspended by top 100 of ' + new URL(window.location.href).host + '",False\n' : ''
return (<li>{i + 1} - {r.domain} - {r.count} blocks</li>)
}
}) : ''}
</ul>
</section >
<Modal domain={domain} reverse={reverse} setSearch={d => setSearchTerm(d)} matrix={prop.matrix} />
</>
)
}
export default Form;

19
front/src/component/Loader.js Archivo normal
Ver fichero

@@ -0,0 +1,19 @@
import { useEffect, useState } from 'react';
import '../loaders.css';
const Loader = () => {
const loaders = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario'],
[load, setLoad] = useState('load ' + loaders[Math.floor(Math.random() * loaders.length)])
useEffect(() => {
setLoad('load ' + loaders[Math.floor(Math.random() * loaders.length)])
}, [])
return (
<div className="loader-content">
<div className="loader">
<div className={load}></div>
</div>
</div>
)
}
export default Loader;

142
front/src/component/Modal.js Archivo normal
Ver fichero

@@ -0,0 +1,142 @@
import { useEffect, useCallback, useState, useRef } from 'react';
import html2canvas from '../../node_modules/html2canvas/dist/html2canvas.js';
import Messenger from '../random-text.js';
const Modal = (prop) => {
let csv
const [blocktitle, setBlocktitle] = useState(''),
[blockcount, setBlockcount] = useState(''),
[blockinstance, setBlockinstance] = useState(''),
[blocktook, setBlocktook] = useState(''),
[blocklist, setBlocklist] = useState([]),
hrefCanvas = useRef(null),
refList = useRef(null),
loading = () => {
document.querySelector('.loader-content').style.display = 'initial'
},
closeModal = event => {
if (event.target.classList.contains('modal') || event.target.classList.contains('closemodal')) {
document.querySelector('.modal-content').style.animationName = 'animatebottom'
}
},
animationEnd = event => {
if (event.animationName === 'animatebottom') {
document.querySelector('.modal').style.display = 'none'
}
},
capture = useCallback(async () => {
const canvas = await html2canvas(hrefCanvas.current, { useCORS: true }),
capture = document.querySelector('.capture')
capture.download = 'fediblock-' + Date.now() + '.png'
capture.href = canvas.toDataURL('image/png', 1.0)
capture.dispatchEvent(new MouseEvent('click'))
}),
download = () => {
const download = document.querySelector('.download')
if (csv.split('\n').length > 2) {
download.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
download.download = 'fediblock-' + prop.domain.domain + '.csv'
}
},
reverse = useCallback(async content => {
if (content && content.length > 0) {
loading()
const result = await fetch('/api/block_count/' + content),
res = await result.json()
if (res && Array.isArray(res.instances)) {
setBlocktitle('Reverse List')
setBlockcount(res.block_count)
setBlockinstance('ing ' + content)
setBlocktook('took ' + res.took + 'ms')
setBlocklist(res.instances)
document.querySelector('.loader-content').style.display = 'none'
document.querySelector('.modal-content').style.animationName = 'animatetop'
document.querySelector('.modal').style.display = 'block'
if (prop.matrix === 'on') {
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
}
}
}
}
}
}),
listblock = useCallback(async (domain) => {
const result = await fetch('/api/detail/' + domain),
res = await result.json()
if (res.blocks && Array.isArray(res.blocks)) {
csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
setBlocktitle('Blocked List')
setBlockcount(res.blocks.length)
setBlocktook('took ' + res.took + 'ms')
setBlockinstance(`ed by ` + (res.api ? `<a href="/api/detail_api/${res.instance}" title="API info for ${res.instance}" target="_blank">${res.instance}</a>`
: res.instance) + (res.nodeinfo ? `&nbsp;<a href="/api/detail_nodeinfo/${res.instance}" title="Nodeinfo for ${res.instance}" target="_blank">&#9432;</a><br />` : `<br />`)
+ `Last update: ` + (new Date(res.last)).toLocaleString())
setBlocklist(res.blocks)
if (prop.matrix === 'on') {
var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
if (walker.currentNode.textContent.length > 1) {
new Messenger(walker.currentNode)
}
}
}
} else {
var a = document.createElement('a')
a.href = '/api/detail_api/' + domain
a.title = 'API info for ' + domain
a.target = '_blank'
a.dispatchEvent(new MouseEvent('click'))
}
document.querySelector('.loader-content').style.display = 'none'
document.querySelector('.modal-content').style.animationName = 'animatetop'
document.querySelector('.modal').style.display = 'block'
})
useEffect(() => {
if (prop.domain && prop.domain.domain.length > 0) {
if (prop.reverse) {
reverse(prop.domain.domain)
} else {
listblock(prop.domain.domain)
}
}
}, [prop.domain, prop.reverse])
return (
<div className="modal" onClick={(e) => closeModal(e)}>
<div className="modal-content" onAnimationEnd={(e) => animationEnd(e)} ref={refList}>
<div className="modal-header">
<span className="closemodal" title="Close" onClick={(e) => closeModal(e)}>&times;</span>
<a className="capture" title="Take snapshot" onClick={capture} type="image/png" target="_blank">&#128247;</a>
{!prop.reverse ? <a className="download" title="Download fediblock CSV mastodon file" onClick={download} type="text/csv" target="_blank">&#8681;</a> : ''}
<h2>{blocktitle}</h2>
</div>
<div className="modal-body" ref={hrefCanvas}>
<p>
<span className="blockcount">{blockcount}</span> public instances are block<span className="blockinstance" dangerouslySetInnerHTML={{ __html: blockinstance }}></span>
<br /><small className="blocktook">{blocktook}</small>
</p>
<ul className="blocklist">{prop.reverse ? blocklist.map((instance, index) => {
return (<li>{index + 1}. <a onClick={() => {
prop.setSearch(instance.instance)
document.querySelector('.modal-content').style.animationName = 'animatebottom'
}}>{instance.instance}</a>{instance.comment ? ' - ' + instance.comment : ''}</li>)
}) : blocklist.map((r, i) => {
if (r?.domain) {
csv += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : ''
return (<li>{i + 1}. <a onClick={() => {
prop.setSearch(r.domain)
document.querySelector('.modal-content').style.animationName = 'animatebottom'
}}>{r.domain}</a>{r.severity ? ' - ' + r.severity : ''}{r.comment ? ' - ' + r.comment : ''}</li>)
}
})}</ul>
</div>
<div className="modal-footer">
</div>
</div>
</div>
)
}
export default Modal;

31
front/src/component/Scan.js Archivo normal
Ver fichero

@@ -0,0 +1,31 @@
import { useEffect, useRef, useState } from 'react';
const Scan = () => {
const [scan, setScan] = useState('Scanning...'),
load = useRef(false)
useEffect(() => {
if (!load.current) {
const source = new window.EventSource('/api/scan')
source.onmessage = event => {
if (event.data) {
if (event.data.length > 0) {
setScan('Async Scanning' + event.data)
} else {
setScan('Async Scanning...')
}
}
}
source.onerror = error => {
console.error(error)
}
load.current = true
}
})
return (
<div>
<span className="scan">{scan}</span>
</div>
)
}
export default Scan;

13
front/src/component/Title.js Archivo normal
Ver fichero

@@ -0,0 +1,13 @@
import { useCallback } from 'react';
const Title = () => {
const click = useCallback(() => {
window.location.hash = ''
window.location.reload(false)
})
return (
<h1><a onClick={() => click()} className="title">Fediblock Instance &#934;</a></h1>
)
}
export default Title;

12
front/src/index.js Archivo normal
Ver fichero

@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();

411
front/src/loaders.css Archivo normal
Ver fichero

@@ -0,0 +1,411 @@
/* HTML: <div class="loader"></div> */
.loader-pong {
width: 80px;
height: 70px;
border: 5px solid #000;
padding: 0 8px;
box-sizing: border-box;
background:
linear-gradient(#fff 0 0) 0 0/8px 20px,
linear-gradient(#fff 0 0) 100% 0/8px 20px,
radial-gradient(farthest-side, #fff 90%, #0000) 0 5px/8px 8px content-box,
#000;
background-repeat: no-repeat;
animation: l3 2s infinite linear;
}
@keyframes l3 {
25% {
background-position: 0 0, 100% 100%, 100% calc(100% - 5px)
}
50% {
background-position: 0 100%, 100% 100%, 0 calc(100% - 5px)
}
75% {
background-position: 0 100%, 100% 0, 100% 5px
}
}
/* HTML: <div class="loader"></div> */
.loader-pacman {
width: 90px;
height: 24px;
padding: 2px 0;
box-sizing: border-box;
display: flex;
animation: l5-0 3s infinite steps(6);
background:
linear-gradient(#000 0 0) 0 0/0% 100% no-repeat,
radial-gradient(circle 3px, #eeee89 90%, #0000) 0 0/20% 100% #000;
overflow: hidden;
}
.loader-pacman::before {
content: "";
width: 20px;
transform: translate(-100%);
border-radius: 50%;
background: #ffff2d;
animation:
l5-1 .25s .153s infinite steps(5) alternate,
l5-2 3s infinite linear;
}
@keyframes l5-1 {
0% {
clip-path: polygon(50% 50%, 100% 0, 100% 0, 0 0, 0 100%, 100% 100%, 100% 100%)
}
100% {
clip-path: polygon(50% 50%, 100% 65%, 100% 0, 0 0, 0 100%, 100% 100%, 100% 35%)
}
}
@keyframes l5-2 {
100% {
transform: translate(90px)
}
}
@keyframes l5-0 {
100% {
background-size: 120% 100%, 20% 100%
}
}
/* HTML: <div class="loader"></div> */
.loader-abyss {
width: 80px;
height: 60px;
box-sizing: border-box;
background:
linear-gradient(#fff 0 0) left /calc(50% - 15px) 8px no-repeat,
linear-gradient(#fff 0 0) right/calc(50% - 15px) 8px no-repeat,
conic-gradient(from 135deg at top, #0000, red 1deg 90deg, #0000 91deg) bottom/14px 8px repeat-x,
#000;
border-bottom: 2px solid red;
position: relative;
overflow: hidden;
animation: l6-0 1s infinite linear;
}
.loader-abyss::before {
content: "";
position: absolute;
width: 10px;
height: 14px;
background: lightblue;
left: -5px;
animation:
l6-1 2s infinite cubic-bezier(0, 100, 1, 100),
l6-2 2s infinite linear;
}
@keyframes l6-0 {
50% {
background-position: left, right, bottom -2px left -4px
}
}
@keyframes l6-1 {
0%,
27% {
bottom: calc(50% + 4px)
}
65%,
100% {
bottom: calc(50% + 4.1px)
}
}
@keyframes l6-2 {
100% {
left: 100%
}
}
/* HTML: <div class="loader"></div> */
.loader-jump {
width: 70px;
height: 50px;
box-sizing: border-box;
background:
conic-gradient(from 135deg at top, #0000, #fff 1deg 90deg, #0000 91deg) right -20px bottom 8px/18px 9px,
linear-gradient(#fff 0 0) bottom/100% 8px,
#000;
background-repeat: no-repeat;
border-bottom: 8px solid #000;
position: relative;
animation: l7-0 2s infinite linear;
}
.loader-jump::before {
content: "";
position: absolute;
width: 10px;
height: 14px;
background: lightblue;
left: 10px;
animation: l7-1 2s infinite cubic-bezier(0, 200, 1, 200);
}
@keyframes l7-0 {
100% {
background-position: left -20px bottom 8px, bottom
}
}
@keyframes l7-1 {
0%,
50% {
bottom: 8px
}
90%,
100% {
bottom: 8.1px
}
}
/* HTML: <div class="loader"></div> */
.loader-loading {
width: fit-content;
font-size: 17px;
font-family: monospace;
line-height: 1.4;
font-weight: bold;
--c: no-repeat linear-gradient(#000 0 0);
background: var(--c), var(--c), var(--c), var(--c), var(--c), var(--c), var(--c);
background-size: calc(1ch + 1px) 100%;
border-bottom: 10px solid #0000;
position: relative;
animation: l8-0 3s infinite linear;
clip-path: inset(-20px 0);
}
.loader-loading::before {
content: "Loading";
}
.loader-loading::after {
content: "";
position: absolute;
width: 10px;
height: 14px;
background: #25adda;
left: -10px;
bottom: 100%;
animation: l8-1 3s infinite linear;
}
@keyframes l8-0 {
0%,
12.5% {
background-position: calc(0*100%/6) 0, calc(1*100%/6) 0, calc(2*100%/6) 0, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0
}
25% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 0, calc(2*100%/6) 0, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0
}
37.5% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 0, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0
}
50% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0
}
62.5% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0
}
75% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 40px, calc(5*100%/6) 0, calc(6*100%/6) 0
}
87.4% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 40px, calc(5*100%/6) 40px, calc(6*100%/6) 0
}
100% {
background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 40px, calc(5*100%/6) 40px, calc(6*100%/6) 40px
}
}
@keyframes l8-1 {
100% {
left: 115%
}
}
/* HTML: <div class="loader"></div> */
.loader-avenger {
width: fit-content;
font-size: 17px;
font-family: monospace;
line-height: 1.4;
font-weight: bold;
background:
linear-gradient(#000 0 0) left,
linear-gradient(#000 0 0) right;
background-repeat: no-repeat;
border-right: 5px solid #0000;
border-left: 5px solid #0000;
background-origin: border-box;
position: relative;
animation: l9-0 2s infinite;
}
.loader-avenger::before {
content: "Loading";
}
.loader-avenger::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 22px;
height: 60px;
background:
linear-gradient(90deg, #000 4px, #0000 0 calc(100% - 4px), #000 0) bottom /22px 20px,
linear-gradient(90deg, red 4px, #0000 0 calc(100% - 4px), red 0) bottom 10px left 0/22px 6px,
linear-gradient(#000 0 0) bottom 3px left 0 /22px 8px,
linear-gradient(#000 0 0) bottom 0 left 50%/8px 16px;
background-repeat: no-repeat;
animation: l9-1 2s infinite;
}
@keyframes l9-0 {
0%,
25% {
background-size: 50% 100%
}
25.1%,
75% {
background-size: 0 0, 50% 100%
}
75.1%,
100% {
background-size: 0 0, 0 0
}
}
@keyframes l9-1 {
25% {
background-position: bottom, bottom 54px left 0, bottom 3px left 0, bottom 0 left 50%;
left: 0
}
25.1% {
background-position: bottom, bottom 10px left 0, bottom 3px left 0, bottom 0 left 50%;
left: 0
}
50% {
background-position: bottom, bottom 10px left 0, bottom 3px left 0, bottom 0 left 50%;
left: calc(100% - 22px)
}
75% {
background-position: bottom, bottom 54px left 0, bottom 3px left 0, bottom 0 left 50%;
left: calc(100% - 22px)
}
75.1% {
background-position: bottom, bottom 10px left 0, bottom 3px left 0, bottom 0 left 50%;
left: calc(100% - 22px)
}
}
/* HTML: <div class="loader"></div> */
.loader-mario {
width: fit-content;
font-size: 17px;
font-family: monospace;
line-height: 1.4;
font-weight: bold;
padding: 30px 2px 50px;
background: linear-gradient(#000 0 0) 0 0/100% 100% content-box padding-box no-repeat;
position: relative;
overflow: hidden;
animation: l10-0 2s infinite cubic-bezier(1, 175, .5, 175);
}
.loader-mario::before {
content: "Loading";
display: inline-block;
animation: l10-2 2s infinite;
}
.loader-mario::after {
content: "";
position: absolute;
width: 34px;
height: 28px;
top: 110%;
left: calc(50% - 16px);
background:
linear-gradient(90deg, #0000 12px, #f92033 0 22px, #0000 0 26px, #fdc98d 0 32px, #0000) bottom 26px left 50%,
linear-gradient(90deg, #0000 10px, #f92033 0 28px, #fdc98d 0 32px, #0000 0) bottom 24px left 50%,
linear-gradient(90deg, #0000 10px, #643700 0 16px, #fdc98d 0 20px, #000 0 22px, #fdc98d 0 24px, #000 0 26px, #f92033 0 32px, #0000 0) bottom 22px left 50%,
linear-gradient(90deg, #0000 8px, #643700 0 10px, #fdc98d 0 12px, #643700 0 14px, #fdc98d 0 20px, #000 0 22px, #fdc98d 0 28px, #f92033 0 32px, #0000 0) bottom 20px left 50%,
linear-gradient(90deg, #0000 8px, #643700 0 10px, #fdc98d 0 12px, #643700 0 16px, #fdc98d 0 22px, #000 0 24px, #fdc98d 0 30px, #f92033 0 32px, #0000 0) bottom 18px left 50%,
linear-gradient(90deg, #0000 8px, #643700 0 12px, #fdc98d 0 20px, #000 0 28px, #f92033 0 30px, #0000 0) bottom 16px left 50%,
linear-gradient(90deg, #0000 12px, #fdc98d 0 26px, #f92033 0 30px, #0000 0) bottom 14px left 50%,
linear-gradient(90deg, #fdc98d 6px, #f92033 0 14px, #222a87 0 16px, #f92033 0 22px, #222a87 0 24px, #f92033 0 28px, #0000 0 32px, #643700 0) bottom 12px left 50%,
linear-gradient(90deg, #fdc98d 6px, #f92033 0 16px, #222a87 0 18px, #f92033 0 24px, #f92033 0 26px, #0000 0 30px, #643700 0) bottom 10px left 50%,
linear-gradient(90deg, #0000 10px, #f92033 0 16px, #222a87 0 24px, #feee49 0 26px, #222a87 0 30px, #643700 0) bottom 8px left 50%,
linear-gradient(90deg, #0000 12px, #222a87 0 18px, #feee49 0 20px, #222a87 0 30px, #643700 0) bottom 6px left 50%,
linear-gradient(90deg, #0000 8px, #643700 0 12px, #222a87 0 30px, #643700 0) bottom 4px left 50%,
linear-gradient(90deg, #0000 6px, #643700 0 14px, #222a87 0 26px, #0000 0) bottom 2px left 50%,
linear-gradient(90deg, #0000 6px, #643700 0 10px, #0000 0) bottom 0px left 50%;
background-size: 34px 2px;
background-repeat: no-repeat;
animation: inherit;
animation-name: l10-1;
}
@keyframes l10-0 {
0%,
30% {
background-position: 0 0px
}
50%,
100% {
background-position: 0 -0.1px
}
}
@keyframes l10-1 {
50%,
100% {
top: 109.5%
}
}
@keyframes l10-2 {
0%,
30% {
transform: translateY(0);
}
80%,
100% {
transform: translateY(-260%);
}
}

1
front/src/logo.svg Archivo normal
Ver fichero

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Después

Anchura:  |  Altura:  |  Tamaño: 2.6 KiB

69
front/src/random-text.js Archivo normal
Ver fichero

@@ -0,0 +1,69 @@
var Messenger = function (el) {
'use strict';
var m = this;
m.init = function () {
m.codeletters = "&#*+%?£@§$";
m.current_length = 0;
m.fadeBuffer = false;
m.message = el.textContent.length > 0 ? el.textContent : ''
setTimeout(m.animateIn, 300);
};
m.generateRandomString = function (length) {
var random_text = '';
while (random_text.length < length) {
random_text += m.codeletters.charAt(Math.floor(Math.random() * m.codeletters.length));
}
return random_text;
};
m.animateIn = function () {
if (m.current_length < m.message.length) {
m.current_length = m.current_length + 2;
if (m.current_length > m.message.length) {
m.current_length = m.message.length;
}
var message = m.generateRandomString(m.current_length);
el.textContent = message;
setTimeout(m.animateIn, 60);
} else {
setTimeout(m.animateFadeBuffer, 60);
}
};
m.animateFadeBuffer = function () {
if (m.fadeBuffer === false) {
m.fadeBuffer = [];
for (var i = 0; i < m.message.length; i++) {
m.fadeBuffer.push({ c: (Math.floor(Math.random() * 12)) + 1, l: m.message.charAt(i) });
}
}
var do_cycles = false;
var message = '';
for (var i = 0; i < m.fadeBuffer.length; i++) {
var fader = m.fadeBuffer[i];
if (fader.c > 0) {
do_cycles = true;
fader.c--;
message += m.codeletters.charAt(Math.floor(Math.random() * m.codeletters.length));
} else {
message += fader.l;
}
}
el.textContent = message;
setTimeout(m.animateFadeBuffer, 150);
};
m.init();
}
module.exports = Messenger;

13
front/src/reportWebVitals.js Archivo normal
Ver fichero

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
front/src/setupTests.js Archivo normal
Ver fichero

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';