initial commit
Todas las comprobaciones han sido exitosas
continuous-integration/drone Build is passing
Todas las comprobaciones han sido exitosas
continuous-integration/drone Build is passing
Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
35
.drone.yml
Archivo normal
35
.drone.yml
Archivo normal
@@ -0,0 +1,35 @@
|
|||||||
|
kind: pipeline
|
||||||
|
name: code-build-linux-amd64
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: docker:dind
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
USER:
|
||||||
|
from_secret: user
|
||||||
|
PASS:
|
||||||
|
from_secret: pass
|
||||||
|
REGISTRY:
|
||||||
|
from_secret: registry
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- docker login -u $USER -p $PASS $REGISTRY
|
||||||
|
- docker buildx build -t $REGISTRY/fediblock-instance .
|
||||||
|
- docker push $REGISTRY/fediblock-instance
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- tag
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
3
.gitignore
vendido
Archivo normal
3
.gitignore
vendido
Archivo normal
@@ -0,0 +1,3 @@
|
|||||||
|
**/node_modules/
|
||||||
|
**/*.lock
|
||||||
|
**/*-lock.json
|
||||||
11
Dockerfile
Archivo normal
11
Dockerfile
Archivo normal
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:22-slim
|
||||||
|
RUN apt update && apt install -y git && apt clean
|
||||||
|
COPY --chown=node:node . /fediblock-instance
|
||||||
|
USER node
|
||||||
|
RUN yarn config set network-timeout 300000
|
||||||
|
WORKDIR /fediblock-instance/front
|
||||||
|
RUN yarn
|
||||||
|
WORKDIR /fediblock-instance/back
|
||||||
|
RUN yarn && yarn build
|
||||||
|
EXPOSE 4000
|
||||||
|
ENTRYPOINT ["yarn", "start"]
|
||||||
42
README.md
Archivo normal
42
README.md
Archivo normal
@@ -0,0 +1,42 @@
|
|||||||
|
# Fediblock Instance
|
||||||
|
|
||||||
|
## Another instance to search public blocks and do stats and more, like one FBA but with NodeJS powers.
|
||||||
|
|
||||||
|
## We need one mongodb instance for federation and one elasticsearch node to storage data.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn or npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build frontend
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build or npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- edit `back/constant.js` with your environment preferences
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start or npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker compose build or docker pull registry.manalejandro.com/fediblock-instance
|
||||||
|
$ docker compose pull
|
||||||
|
$ docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best deploy with subdomain in one HTTPS proxy like nginx or apache throught 4000/tcp port
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
17
back/constant.js
Archivo normal
17
back/constant.js
Archivo normal
@@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
index: 'fediblock', // elasticsearch index name
|
||||||
|
nick: 'fediblockbot', // nick of bot
|
||||||
|
icon: 'https://manalejandro.com/favicon.png', // icon of the instance
|
||||||
|
dburl: 'mongodb://fediblock-mongodb:27017', // mongodb connection
|
||||||
|
dbname: 'fediblock', // mongodb database name
|
||||||
|
apexdomain: 'fediblock.manalejandro.com', // domain of the instance
|
||||||
|
agent: 'fediblock.manalejandro.com', // agent of fetch requests
|
||||||
|
elasticnode: 'http://fediblock-elasticsearch:9200', // elasticsearch connection
|
||||||
|
workers: 3, // async concurrent workers to scan
|
||||||
|
threads: 2, // threads of each worker
|
||||||
|
schedule: '5,11,17,23', // UTC hours to publish bot followers federated stats
|
||||||
|
taskdeletedup: '6', // task hour to delete dups index instances entries
|
||||||
|
filterdomains: ['activitypub-troll.cf'], // domains filtered to scan
|
||||||
|
initialscan: 'mastodon.social', // initial federated domain to scan
|
||||||
|
abort_timeout: 5000 // Timeout of abort scan request
|
||||||
|
}
|
||||||
7191
back/fediblock-mapping.json
Archivo normal
7191
back/fediblock-mapping.json
Archivo normal
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
3
back/index.js
Archivo normal
3
back/index.js
Archivo normal
@@ -0,0 +1,3 @@
|
|||||||
|
const server = require('./lib/express')
|
||||||
|
|
||||||
|
console.log('Server listening on ' + server.address().address + ':' + server.address().port)
|
||||||
33
back/lib/apex.js
Archivo normal
33
back/lib/apex.js
Archivo normal
@@ -0,0 +1,33 @@
|
|||||||
|
const ActivitypubExpress = require('activitypub-express'),
|
||||||
|
constant = require('../constant'),
|
||||||
|
routes = {
|
||||||
|
actor: '/u/:actor',
|
||||||
|
object: '/o/:id',
|
||||||
|
activity: '/s/:id',
|
||||||
|
inbox: '/u/:actor/inbox',
|
||||||
|
outbox: '/u/:actor/outbox',
|
||||||
|
followers: '/u/:actor/followers',
|
||||||
|
following: '/u/:actor/following',
|
||||||
|
liked: '/u/:actor/liked',
|
||||||
|
collections: '/u/:actor/c/:id',
|
||||||
|
blocked: '/u/:actor/blocked',
|
||||||
|
rejections: '/u/:actor/rejections',
|
||||||
|
rejected: '/u/:actor/rejected',
|
||||||
|
shares: '/s/:id/shares',
|
||||||
|
likes: '/s/:id/likes'
|
||||||
|
},
|
||||||
|
apex = ActivitypubExpress({
|
||||||
|
name: 'Fediblock Instance',
|
||||||
|
version: '1.0.0',
|
||||||
|
domain: constant.apexdomain,
|
||||||
|
actorParam: 'actor',
|
||||||
|
objectParam: 'id',
|
||||||
|
activityParam: 'id',
|
||||||
|
routes,
|
||||||
|
endpoints: {
|
||||||
|
proxyUrl: 'https://' + constant.apexdomain + '/proxy'
|
||||||
|
},
|
||||||
|
itemsPerPage: 200
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = { routes, apex }
|
||||||
97
back/lib/apexcustom.js
Archivo normal
97
back/lib/apexcustom.js
Archivo normal
@@ -0,0 +1,97 @@
|
|||||||
|
// custom side-effects for your app
|
||||||
|
module.exports = (app, apex, client) => {
|
||||||
|
const { sendFederatedMessage, requestPart } = require('./util'),
|
||||||
|
constant = require('../constant')
|
||||||
|
app.on('apex-outbox', msg => {
|
||||||
|
if (typeof msg === 'object'
|
||||||
|
&& !Object.keys(msg).filter(prop => msg[prop]
|
||||||
|
&& typeof msg[prop] === 'object').some(prop => !(msg[prop].hasOwnProperty('id')
|
||||||
|
&& msg[prop].hasOwnProperty('type')))
|
||||||
|
&& msg.activity
|
||||||
|
&& msg.activity.type) {
|
||||||
|
console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`)
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(msg, '', 2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.on('apex-inbox', async msg => {
|
||||||
|
if (typeof msg === 'object'
|
||||||
|
&& !Object.keys(msg).filter(prop => msg[prop]
|
||||||
|
&& typeof msg[prop] === 'object').some(prop => !(msg[prop].hasOwnProperty('id')
|
||||||
|
&& msg[prop].hasOwnProperty('type')))
|
||||||
|
&& msg.activity
|
||||||
|
&& msg.activity.type) {
|
||||||
|
const type = msg.activity.type.toLowerCase()
|
||||||
|
if (type === 'follow' && msg.recipient.type.toLowerCase() === 'person') {
|
||||||
|
await apex.acceptFollow(msg.recipient, msg.activity)
|
||||||
|
const act = await apex.buildActivity('Accept', apex.utils.usernameToIRI(constant.nick), ['https://www.w3.org/ns/activitystreams#Public'].concat([msg.actor.id]), {
|
||||||
|
actor: msg.recipient,
|
||||||
|
object: msg.activity
|
||||||
|
})
|
||||||
|
await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true), act)
|
||||||
|
}
|
||||||
|
if (type === 'create' && msg.object.type.toLowerCase() === 'note' && msg.actor.preferredUsername && msg.object.content) {
|
||||||
|
const name = Array.isArray(msg.actor.preferredUsername) ? msg.actor.preferredUsername[0] : msg.actor.preferredUsername,
|
||||||
|
content = ('' + msg.object.content).replace(/<[^>]*>?/gm, '').split(' ').filter(token => !token.startsWith('@'))
|
||||||
|
if (msg.recipient.id === apex.utils.usernameToIRI(constant.nick)) {
|
||||||
|
if (content.length === 1) {
|
||||||
|
try {
|
||||||
|
const instance = content.join('').trim()
|
||||||
|
if (instance === 'stats') {
|
||||||
|
const statsres = await requestPart('https://' + constant.apexdomain + '/api/stats')
|
||||||
|
await sendFederatedMessage(constant.nick, null, `STATS\n
|
||||||
|
Statuses AVG: ${Math.round(statsres.status_avg)}
|
||||||
|
Statuses MAX: ${statsres.status_max}
|
||||||
|
Domain AVG: ${Math.round(statsres.domain_avg)}
|
||||||
|
Domain MAX: ${statsres.domain_max}
|
||||||
|
Users AVG: ${Math.round(statsres.user_avg)}
|
||||||
|
Users MAX: ${statsres.user_max}
|
||||||
|
Stats Instances: ${statsres.stats_filtered}
|
||||||
|
Total Instances: ${statsres.instance_count}
|
||||||
|
Users by Instance: ${(Math.round(statsres.user_avg) / statsres.instance_count).toFixed(2)}
|
||||||
|
Statuses by Domain: ${(Math.round(statsres.status_avg) / Math.round(statsres.domain_avg)).toFixed(2)}
|
||||||
|
Statuses by User: ${(Math.round(statsres.status_avg) / Math.round(statsres.user_avg)).toFixed(2)}
|
||||||
|
https://${constant.apexdomain}`, msg.actor.id, msg.object.id)
|
||||||
|
} else {
|
||||||
|
const json = await requestPart('https://' + constant.apexdomain + '/api/detail/' + instance)
|
||||||
|
if (json && json.blocks && Array.isArray(json.blocks) && json.blocks.length > 0) {
|
||||||
|
const res = await requestPart('https://' + constant.apexdomain + '/api/list/' + instance)
|
||||||
|
if (res && res.instances && Array.isArray(res.instances)) {
|
||||||
|
res.instances.map(async r => {
|
||||||
|
if (r.domain === instance) {
|
||||||
|
await sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks\n'
|
||||||
|
+ (r.api.title ? '\n'
|
||||||
|
+ r.api.title + ' - ' + r.api.uri + '\n'
|
||||||
|
+ (r.api.email ? 'Email: ' + r.api.email + '\n' : '')
|
||||||
|
+ 'Registration: ' + (r.api.registrations ? 'open' : 'closed') + ' - Version: ' + r.api.version + '\n'
|
||||||
|
+ (r.api.stats ? 'Users: ' + r.api.stats.user_count + ' - Statuses: ' + r.api.stats.status_count + ' - Domains: ' + r.api.stats.domain_count + '\n' : '')
|
||||||
|
+ (r.api.description ? 'Description: ' + r.api.description + '\n' : '') : '')
|
||||||
|
+ 'https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks - https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' not found, next try.', msg.actor.id, msg.object.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await sendFederatedMessage(constant.nick, null, e.message, msg.actor.id, msg.object.id)
|
||||||
|
// console.error(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await sendFederatedMessage(constant.nick, 'Hi ' + name, 'I know ' + (await client.count({ index: constant.index })).count + ' Federated Instances\nScanning ' + app.locals.server + ' instance with ' + app.locals.scantotal + ' peers\nScanned ' + app.locals.peers + ' peers from ' + app.locals.instances + ' instances, ' + app.locals.created + ' created, ' + app.locals.updated + ' updated\nhttps://' + constant.apexdomain, msg.actor.id, msg.object.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`New note from ${name} to ${msg.recipient.id}: ${content.join(' ')}`)
|
||||||
|
} else if (type !== 'delete') {
|
||||||
|
console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`)
|
||||||
|
} else {
|
||||||
|
console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(msg, '', 2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
54
back/lib/api.js
Archivo normal
54
back/lib/api.js
Archivo normal
@@ -0,0 +1,54 @@
|
|||||||
|
module.exports = (app, apex) => {
|
||||||
|
const constant = require('../constant'),
|
||||||
|
{ readFile, writeFile } = require('fs'),
|
||||||
|
asyncHandler = require('express-async-handler')
|
||||||
|
app.get('/api/scan', (req, res) => {
|
||||||
|
res.header('Content-Type', 'text/event-stream')
|
||||||
|
res.header('Connection', 'keep-alive')
|
||||||
|
res.header('Cache-Control', 'no-cache')
|
||||||
|
res.header('X-Accel-Buffering', 'no')
|
||||||
|
res.write(Buffer.from('data: ...\n\n'))
|
||||||
|
req.app.locals.scan.on('data', data => {
|
||||||
|
res.write(Buffer.from(data))
|
||||||
|
})
|
||||||
|
res.setTimeout(20000, () => {
|
||||||
|
res.write(Buffer.from('data: ...\n\n'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
app.get('/api/served', (req, res, next) => {
|
||||||
|
readFile(__dirname + '/../served.txt', async (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
next(err)
|
||||||
|
} else {
|
||||||
|
const num = parseInt(data.toString()) + 1
|
||||||
|
writeFile(__dirname + '/../served.txt', num.toString(), 'utf8', (err) => {
|
||||||
|
if (err) {
|
||||||
|
next(err)
|
||||||
|
} else {
|
||||||
|
res.json({ served: num, lastscan: req.app.locals.scantotal, server: req.app.locals.server, instances: req.app.locals.instances, peers: req.app.locals.peers, created: req.app.locals.created, updated: req.app.locals.updated })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
app.get('/api/outbox', asyncHandler(async (req, res, next) => {
|
||||||
|
const outbox = await apex.getOutbox(await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true), 'true'),
|
||||||
|
notes = outbox.orderedItems.filter(e => e.object
|
||||||
|
&& e.object.length > 0
|
||||||
|
&& e.object[0].type === 'Note'
|
||||||
|
&& e.object[0].content
|
||||||
|
&& e.object[0].content[0].startsWith('<p>You')).slice(0, 20).map(e => ({
|
||||||
|
content: e.object[0].content[0],
|
||||||
|
published: e.published[0]
|
||||||
|
}))
|
||||||
|
res.json(notes)
|
||||||
|
}))
|
||||||
|
app.get('/api/inbox', asyncHandler(async (req, res, next) => {
|
||||||
|
const actor = await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true)
|
||||||
|
actor._local = {}
|
||||||
|
actor._local.blockList = []
|
||||||
|
const inbox = await apex.getInbox(actor, 'true'),
|
||||||
|
notes = inbox.orderedItems.filter(e => e.object && e.object.length > 0 && e.object[0].type === 'Note').slice(0, 20).map(e => e.object[0].content[0])
|
||||||
|
res.json(notes)
|
||||||
|
}))
|
||||||
|
}
|
||||||
545
back/lib/apiswagger.js
Archivo normal
545
back/lib/apiswagger.js
Archivo normal
@@ -0,0 +1,545 @@
|
|||||||
|
module.exports = (app, client) => {
|
||||||
|
const constant = require('../constant'),
|
||||||
|
zlib = require('zlib'),
|
||||||
|
asyncHandler = require('express-async-handler'),
|
||||||
|
{ param, validationResult } = require('express-validator')
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/stats:
|
||||||
|
* get:
|
||||||
|
* summary: Retrieve stats of instances.
|
||||||
|
* description: Retrieve stats of total instances.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A object with stats parameters.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* instance_count:
|
||||||
|
* type: integer
|
||||||
|
* description: Count of instances.
|
||||||
|
* example: 0
|
||||||
|
* status_avg:
|
||||||
|
* type: float
|
||||||
|
* description: Average statuses of total instances.
|
||||||
|
* example: 0
|
||||||
|
* status_max:
|
||||||
|
* type: integer
|
||||||
|
* description: Max of total statuses instances.
|
||||||
|
* example: 0
|
||||||
|
* user_avg:
|
||||||
|
* type: float
|
||||||
|
* description: Average users of total instances.
|
||||||
|
* example: 0
|
||||||
|
* user_max:
|
||||||
|
* type: integer
|
||||||
|
* description: Max of total users instances.
|
||||||
|
* example: 0
|
||||||
|
* domain_avg:
|
||||||
|
* type: float
|
||||||
|
* description: Number of average domains instances.
|
||||||
|
* example: 0
|
||||||
|
* domain_max:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of max domains instances.
|
||||||
|
* example: 0
|
||||||
|
* stats_filtered:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of instances stats.
|
||||||
|
* example: 0
|
||||||
|
*/
|
||||||
|
app.get('/api/stats', asyncHandler(async (req, res) => {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
body: {
|
||||||
|
size: 0,
|
||||||
|
aggs: {
|
||||||
|
instance_count: {
|
||||||
|
value_count: {
|
||||||
|
field: '_id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stats_filtered: {
|
||||||
|
filter: {
|
||||||
|
exists: {
|
||||||
|
field: 'api.stats'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user_avg: {
|
||||||
|
avg: {
|
||||||
|
field: 'api.stats.user_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user_max: {
|
||||||
|
max: {
|
||||||
|
field: 'api.stats.user_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
domain_avg: {
|
||||||
|
avg: {
|
||||||
|
field: 'api.stats.domain_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
domain_max: {
|
||||||
|
max: {
|
||||||
|
field: 'api.stats.domain_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status_avg: {
|
||||||
|
avg: {
|
||||||
|
field: 'api.stats.status_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status_max: {
|
||||||
|
max: {
|
||||||
|
field: 'api.stats.status_count'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res.json(Object.keys(result.aggregations).reduce((prev, curr) => ({
|
||||||
|
...prev, [curr]: result.aggregations[curr][Object.keys(result.aggregations[curr])[0]]
|
||||||
|
}), {}))
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/count:
|
||||||
|
* get:
|
||||||
|
* summary: Retrieve count of instances.
|
||||||
|
* description: Retrieve a number of total instances.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A object with count parameter.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* count:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of instances.
|
||||||
|
* example: 0
|
||||||
|
*/
|
||||||
|
app.get('/api/count', asyncHandler(async (req, res) => {
|
||||||
|
res.json({ count: (await client.count({ index: constant.index })).count })
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/ranking:
|
||||||
|
* get:
|
||||||
|
* summary: Retrieve one ranking of instances.
|
||||||
|
* description: Retrieve a top ten ranking of total fediblock instances.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A object with count parameter.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* domain:
|
||||||
|
* type: string
|
||||||
|
* description: Domain of the instance.
|
||||||
|
* example: "mastodon.social"
|
||||||
|
* count:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of fediblocks.
|
||||||
|
* example: 0
|
||||||
|
*/
|
||||||
|
app.get('/api/ranking', asyncHandler(async (req, res) => {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
body: {
|
||||||
|
size: 0,
|
||||||
|
aggs: {
|
||||||
|
blocks: {
|
||||||
|
nested: {
|
||||||
|
path: 'blocks'
|
||||||
|
},
|
||||||
|
aggs: {
|
||||||
|
ranking: {
|
||||||
|
terms: {
|
||||||
|
field: 'blocks.domain',
|
||||||
|
size: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const ranking = result.aggregations?.blocks?.ranking?.buckets
|
||||||
|
res.json(ranking.map(r => ({ domain: r.key, count: r.doc_count })))
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/list/{instance}:
|
||||||
|
* get:
|
||||||
|
* summary: Search result array of intances.
|
||||||
|
* description: Retrieve a result array of instances matching search input.
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: instance
|
||||||
|
* required: true
|
||||||
|
* description: String for search instance
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A list of instances.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* instances:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: data
|
||||||
|
* description: List of instances.
|
||||||
|
* example: "mastodon.social"
|
||||||
|
* suggests:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: data
|
||||||
|
* description: Suggest of the instance.
|
||||||
|
* example: "mastodon.social"
|
||||||
|
*/
|
||||||
|
app.get('/api/list/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
errors: errors.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 10,
|
||||||
|
query: {
|
||||||
|
wildcard: {
|
||||||
|
instance: {
|
||||||
|
value: `*${req.params.instance}*`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
suggest: {
|
||||||
|
suggests: {
|
||||||
|
text: req.params.instance,
|
||||||
|
term: {
|
||||||
|
field: 'instance',
|
||||||
|
size: 3,
|
||||||
|
sort: 'score',
|
||||||
|
suggest_mode: 'always',
|
||||||
|
max_edits: 2,
|
||||||
|
min_word_length: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [],
|
||||||
|
suggests = result.suggest.suggests && result.suggest.suggests.length > 0 && result.suggest.suggests[0].options.length > 0 ? result.suggest.suggests[0].options : []
|
||||||
|
res.json({
|
||||||
|
instances: instances.map(instance => ({
|
||||||
|
domain: instance._source.instance,
|
||||||
|
api: instance._source.api ? instance._source.api : null,
|
||||||
|
blocks: instance._source.blocks && instance._source.blocks.length > 0 ? instance._source.blocks.length : null,
|
||||||
|
last: instance._source.last ? instance._source.last : null,
|
||||||
|
nodeinfo: instance._source.nodeinfo ? true : false
|
||||||
|
})), suggests: suggests.map(instance => instance.text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/detail/{instance}:
|
||||||
|
* get:
|
||||||
|
* summary: Search result array of fediblocks intances.
|
||||||
|
* description: Retrieve a result array of fediblock instances matching search input.
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: instance
|
||||||
|
* required: true
|
||||||
|
* description: String to detail of the instance
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A list of instances.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* domain:
|
||||||
|
* type: string
|
||||||
|
* description: Domain of the block instance.
|
||||||
|
* example: "mastodon.social"
|
||||||
|
* comment:
|
||||||
|
* type: string
|
||||||
|
* description: Comment of the block instance.
|
||||||
|
* example: "mastodon.social"
|
||||||
|
* severity:
|
||||||
|
* type: string
|
||||||
|
* description: Severity of the block instance.
|
||||||
|
* example: "mastodon.social"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
app.get('/api/detail/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
errors: errors.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: req.params.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits?.hits?.length > 0 ? result.hits.hits : []
|
||||||
|
res.json(instances.length > 0 && instances[0]._source.blocks && instances[0]._source.blocks.length > 0 ?
|
||||||
|
{
|
||||||
|
blocks: instances[0]._source.blocks.map(instance => ({ domain: instance.domain, comment: instance.comment, severity: instance.severity })),
|
||||||
|
last: instances[0]._source.last,
|
||||||
|
instance: instances[0]._source.instance,
|
||||||
|
nodeinfo: instances[0]._source.nodeinfo,
|
||||||
|
api: instances[0]._source.api,
|
||||||
|
took: result.took
|
||||||
|
} : [])
|
||||||
|
} else {
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/detail_api/{instance}:
|
||||||
|
* get:
|
||||||
|
* summary: Search result of detail's api intance.
|
||||||
|
* description: Retrieve a result of detail's api instance.
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: instance
|
||||||
|
* required: true
|
||||||
|
* description: String to detail of the instance
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Detail of the api instance.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
*/
|
||||||
|
app.get('/api/detail_api/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
errors: errors.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: req.params.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits?.hits?.length > 0 ? result.hits.hits : []
|
||||||
|
res.json(instances.length > 0 ? instances[0]._source.api : {})
|
||||||
|
} else {
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/detail_nodeinfo/{instance}:
|
||||||
|
* get:
|
||||||
|
* summary: Search result of detail's nodeinfo intance.
|
||||||
|
* description: Retrieve a result of detail's nodeinfo instance.
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: instance
|
||||||
|
* required: true
|
||||||
|
* description: String to detail of the instance
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Detail of the nodeinfo instance.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
*/
|
||||||
|
app.get('/api/detail_nodeinfo/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
errors: errors.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: req.params.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits?.hits?.length > 0 ? result.hits.hits : []
|
||||||
|
res.json(instances.length > 0 ? instances[0]._source.nodeinfo : {})
|
||||||
|
} else {
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/block_count/{instance}:
|
||||||
|
* get:
|
||||||
|
* summary: Retrieve count of fediblocked instances.
|
||||||
|
* description: Retrieve a number of total fediblocked instances.
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: instance
|
||||||
|
* required: true
|
||||||
|
* description: String to search fediblocks of the instance
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A object with block_count parameter.
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* block_count:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of fediblock instances.
|
||||||
|
* example: 0
|
||||||
|
* instances:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* instance:
|
||||||
|
* type: string
|
||||||
|
* description: Instance of the block.
|
||||||
|
* example: mastodon.social
|
||||||
|
* comment:
|
||||||
|
* type: string
|
||||||
|
* description: Comment about the block.
|
||||||
|
* example: spam
|
||||||
|
*/
|
||||||
|
app.get('/api/block_count/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
errors: errors.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 9999,
|
||||||
|
query: {
|
||||||
|
nested: {
|
||||||
|
path: 'blocks',
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
'blocks.domain': req.params.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [],
|
||||||
|
instancescomment = instances.map(i => ({
|
||||||
|
instance: i._source.instance, comment: i._source.blocks.find(block => block.domain === req.params.instance).comment
|
||||||
|
}))
|
||||||
|
res.json({
|
||||||
|
block_count: instances.length,
|
||||||
|
instances: instancescomment,
|
||||||
|
took: result.took
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/download_index:
|
||||||
|
* get:
|
||||||
|
* summary: Retrieve all content of ElasticSearch index.
|
||||||
|
* description: Retrieve all content of ElasticSearch index.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: A file compressed with gzip.
|
||||||
|
* content:
|
||||||
|
* application/gzip:
|
||||||
|
*/
|
||||||
|
app.get('/api/download_index', asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.setHeader('Content-Type', 'application/gzip')
|
||||||
|
res.setHeader('Content-disposition', 'attachment; filename=fediblock-index.json.gz')
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 9999,
|
||||||
|
query: {
|
||||||
|
match_all: {}
|
||||||
|
}
|
||||||
|
}, { asStream: true, meta: false })
|
||||||
|
result.pipe(zlib.createGzip()).pipe(res)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
158
back/lib/express.js
Archivo normal
158
back/lib/express.js
Archivo normal
@@ -0,0 +1,158 @@
|
|||||||
|
const http = require('http'),
|
||||||
|
express = require('express'),
|
||||||
|
rateLimit = require('express-rate-limit'),
|
||||||
|
app = express(),
|
||||||
|
events = require('events'),
|
||||||
|
constant = require('../constant'),
|
||||||
|
logger = require('./logger'),
|
||||||
|
{ routes, apex } = require('./apex'),
|
||||||
|
apexcustom = require('./apexcustom'),
|
||||||
|
apiswagger = require('./apiswagger'),
|
||||||
|
api = require('./api'),
|
||||||
|
swagger = require('./swagger'),
|
||||||
|
fediblock = require('./fediblock'),
|
||||||
|
taskdeletedup = require('./taskdeletedup'),
|
||||||
|
{ generateKeyPairSync } = require('crypto'),
|
||||||
|
{ MongoClient } = require('mongodb'),
|
||||||
|
mongoclient = new MongoClient(constant.dburl),
|
||||||
|
{ Client } = require('@elastic/elasticsearch'),
|
||||||
|
client = new Client({
|
||||||
|
node: constant.elasticnode,
|
||||||
|
pingTimeout: 10000,
|
||||||
|
requestTimeout: 60000,
|
||||||
|
retryOnTimeout: true,
|
||||||
|
maxRetries: 3
|
||||||
|
}),
|
||||||
|
register = async () => {
|
||||||
|
const admin = await apex.store.getObject(apex.utils.usernameToIRI('admin'), true)
|
||||||
|
if (!admin) {
|
||||||
|
const adminaccount = await apex.createActor('admin', 'Fediblock Admin', 'Fediblock Admin - https://' + constant.apexdomain,
|
||||||
|
{ type: 'Image', mediaType: 'image/png', url: constant.icon }, 'Service'),
|
||||||
|
keys = generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'spki',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
adminaccount.publicKey[0].publicKeyPem[0] = keys.publicKey
|
||||||
|
adminaccount._meta.privateKey = keys.privateKey
|
||||||
|
await apex.store.saveObject(adminaccount)
|
||||||
|
const bot = await apex.createActor(constant.nick, 'Fediblock Bot', 'Fediblock #Bot - https://' + constant.apexdomain,
|
||||||
|
{ type: 'Image', mediaType: 'image/png', url: constant.icon }, 'Person'),
|
||||||
|
keysbot = generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'spki',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bot.publicKey[0].publicKeyPem[0] = keysbot.publicKey
|
||||||
|
bot._meta.privateKey = keysbot.privateKey
|
||||||
|
await apex.store.saveObject(bot)
|
||||||
|
apex.systemUser = adminaccount
|
||||||
|
apex.store.setup(adminaccount)
|
||||||
|
} else {
|
||||||
|
apex.systemUser = admin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
connect = () => {
|
||||||
|
mongoclient.connect()
|
||||||
|
.then(async () => {
|
||||||
|
const exists = await client.indices.exists({ index: constant.index })
|
||||||
|
if (!exists) {
|
||||||
|
await client.indices.create({
|
||||||
|
index: constant.index,
|
||||||
|
body: require('../fediblock-mapping.json')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
apex.store.db = mongoclient.db(constant.dbname)
|
||||||
|
return apex.store.db
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await register()
|
||||||
|
taskdeletedup(client)
|
||||||
|
await apex.startDelivery()
|
||||||
|
await fediblock(client, app)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
server = http.createServer(app).listen(4000, () => {
|
||||||
|
connect()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.locals.scan = new events.EventEmitter()
|
||||||
|
app.locals.scannum = 0
|
||||||
|
app.locals.scantotal = 0
|
||||||
|
app.locals.server = ''
|
||||||
|
app.locals.peers = 0
|
||||||
|
app.locals.instances = 0
|
||||||
|
app.locals.created = 0
|
||||||
|
app.locals.updated = 0
|
||||||
|
app.disable('x-powered-by')
|
||||||
|
app.set('json spaces', 2)
|
||||||
|
app.set('trust proxy', 1)
|
||||||
|
app.use(logger)
|
||||||
|
app.use(rateLimit({
|
||||||
|
windowMs: 1 * 60 * 1000, // 1 minutes
|
||||||
|
limit: 120, // each IP can make up to 120 requests per `windowsMs` (5 minutes)
|
||||||
|
standardHeaders: true, // add the `RateLimit-*` headers to the response
|
||||||
|
legacyHeaders: false,
|
||||||
|
delayAfter: 30, // allow 30 requests per `windowMs` (5 minutes) without slowing them down
|
||||||
|
delayMs: (hits) => hits * 200, // add 200 ms of delay to every request after the 10th
|
||||||
|
maxDelayMs: 5000
|
||||||
|
}))
|
||||||
|
app.use(
|
||||||
|
express.json({ type: apex.consts.jsonldTypes }),
|
||||||
|
express.urlencoded({ extended: true }),
|
||||||
|
apex
|
||||||
|
)
|
||||||
|
app.route(routes.inbox)
|
||||||
|
.get(apex.net.inbox.get)
|
||||||
|
.post(apex.net.inbox.post)
|
||||||
|
app.route(routes.outbox)
|
||||||
|
.get(apex.net.outbox.get)
|
||||||
|
.post(apex.net.outbox.post)
|
||||||
|
app.get(routes.actor, apex.net.actor.get)
|
||||||
|
app.get(routes.followers, apex.net.followers.get)
|
||||||
|
app.get(routes.following, apex.net.following.get)
|
||||||
|
app.get(routes.liked, apex.net.liked.get)
|
||||||
|
app.get(routes.object, apex.net.object.get)
|
||||||
|
app.get(routes.activity, apex.net.activityStream.get)
|
||||||
|
app.get(routes.shares, apex.net.shares.get)
|
||||||
|
app.get(routes.likes, apex.net.likes.get)
|
||||||
|
app.get('/.well-known/webfinger', apex.net.webfinger.get)
|
||||||
|
app.get('/.well-known/nodeinfo', apex.net.nodeInfoLocation.get)
|
||||||
|
app.get('/nodeinfo/:version', apex.net.nodeInfo.get)
|
||||||
|
app.post('/proxy', apex.net.proxy.post)
|
||||||
|
apexcustom(app, apex, client)
|
||||||
|
api(app, apex, client)
|
||||||
|
apiswagger(app, client)
|
||||||
|
swagger(app)
|
||||||
|
app.get('/u/' + constant.nick, (req, res) => {
|
||||||
|
res.redirect('/api/inbox')
|
||||||
|
})
|
||||||
|
app.get(Object.keys(routes).map(route => routes[route]), (req, res) => {
|
||||||
|
res.redirect('/')
|
||||||
|
})
|
||||||
|
app.use(express.static(__dirname + '/../../front/build'))
|
||||||
|
.use((err, req, res, next) => {
|
||||||
|
if (res.headersSent) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return next(err)
|
||||||
|
}
|
||||||
|
res.status(500).end('Error')
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = server
|
||||||
211
back/lib/fediblock.js
Archivo normal
211
back/lib/fediblock.js
Archivo normal
@@ -0,0 +1,211 @@
|
|||||||
|
module.exports = async (client, app) => {
|
||||||
|
const { sendFederatedMessage, requestPart } = require('./util'),
|
||||||
|
constant = require('../constant'),
|
||||||
|
schedule = require('node-schedule'),
|
||||||
|
{ PromisePool } = require('@supercharge/promise-pool'),
|
||||||
|
workers = constant.workers,
|
||||||
|
threads = constant.threads,
|
||||||
|
getAccount = api => {
|
||||||
|
if (api && api.contact_account.acct) {
|
||||||
|
const acct = api.contact_account.acct.split('@')
|
||||||
|
if (acct.length > 1) {
|
||||||
|
return `https://${acct[1]}/users/${acct[0]}`
|
||||||
|
} else {
|
||||||
|
return `https://${api.uri.replace(/https?:\/\//, '')}/users/${acct.join('')}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanInstance = async instance => {
|
||||||
|
const json = await requestPart(`https://${instance}/api/v1/instance/domain_blocks`)
|
||||||
|
if (Array.isArray(json) && json.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
instancelocated = result.hits && result.hits.hits ? result.hits.hits : [],
|
||||||
|
blocks = json.map(block => {
|
||||||
|
if (block.comment && block.comment.length > 8190) {
|
||||||
|
block.comment = block.comment.slice(0, 8190)
|
||||||
|
}
|
||||||
|
return block
|
||||||
|
}),
|
||||||
|
[api, nodeinfo, peers] = await Promise.all([
|
||||||
|
requestPart(`https://${instance}/api/v1/instance`),
|
||||||
|
requestPart(`https://${instance}/nodeinfo/2.0`),
|
||||||
|
requestPart(`https://${instance}/api/v1/instance/peers`)
|
||||||
|
])
|
||||||
|
if (result && result.hits) {
|
||||||
|
if (instancelocated.length === 0) {
|
||||||
|
await client.index({
|
||||||
|
index: constant.index,
|
||||||
|
body: {
|
||||||
|
instance,
|
||||||
|
api,
|
||||||
|
nodeinfo,
|
||||||
|
blocks,
|
||||||
|
peers,
|
||||||
|
last: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.locals.created++
|
||||||
|
return sendFederatedMessage(constant.nick, 'New Fediblock Instance', `Fediblock Instance ${instance} with ${json.length} blocks - https://${constant.apexdomain}#${instance}`, getAccount(api))
|
||||||
|
} else {
|
||||||
|
const elasticinstance = instancelocated[0]._source.blocks || []
|
||||||
|
if (Array.isArray(elasticinstance)) {
|
||||||
|
if (json.length !== elasticinstance.length
|
||||||
|
|| (instancelocated[0]._source.last && instancelocated[0]._source.last < new Date(Date.now() - 2678400000))
|
||||||
|
|| !instancelocated[0]._source.api
|
||||||
|
|| !instancelocated[0]._source.nodeinfo
|
||||||
|
|| !instancelocated[0]._source.peers) {
|
||||||
|
await client.update({
|
||||||
|
index: constant.index,
|
||||||
|
id: instancelocated[0]._id,
|
||||||
|
doc: {
|
||||||
|
api: api ? api : instancelocated[0]._source.api,
|
||||||
|
nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo,
|
||||||
|
blocks: blocks && blocks.length > 0 ? blocks : elasticinstance,
|
||||||
|
peers: peers && peers.length > 0 ? peers : instancelocated[0]._source.peers,
|
||||||
|
last: new Date()
|
||||||
|
},
|
||||||
|
doc_as_upsert: true
|
||||||
|
})
|
||||||
|
app.locals.updated++
|
||||||
|
if (instancelocated[0]._source.api && instancelocated[0]._source.api.uri && instancelocated[0]._source.api.contact_account && instancelocated[0]._source.api.contact_account.acct) {
|
||||||
|
const difference = blocks.filter(block => block.domain && block.domain.trim().length > 0 && !elasticinstance.some(instance => block.domain === instance.domain))
|
||||||
|
if (difference.length > 0 && difference.length < 20) {
|
||||||
|
return sendFederatedMessage(constant.nick, 'Detected #Fediblock by Fediblock Instance', `You blocked new instances: ${difference.map(d => d.domain).join(', ')} - https://${constant.apexdomain}#${instance}`, getAccount(instancelocated[0]._source.api))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanPart = async instancesall => PromisePool
|
||||||
|
.withConcurrency(threads)
|
||||||
|
.for(instancesall)
|
||||||
|
.process(async instance => {
|
||||||
|
app.locals.scan.emit('data', 'data: ' + (app.locals.scannum > 0 ? '(' + app.locals.scannum-- + '): ' + instance : ': ' + instance) + '\n\n')
|
||||||
|
app.locals.peers++
|
||||||
|
return scanInstance(instance)
|
||||||
|
}),
|
||||||
|
scanIndex = async (server, instancesall) => {
|
||||||
|
const [api, nodeinfo, blocks] = await Promise.all([
|
||||||
|
requestPart(`https://${server}/api/v1/instance`),
|
||||||
|
requestPart(`https://${server}/nodeinfo/2.0`),
|
||||||
|
requestPart(`https://${server}/api/v1/instance/domain_blocks`)
|
||||||
|
])
|
||||||
|
if (api && typeof api === 'object' && api.version) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
instancelocated = result.hits && result.hits.hits ? result.hits.hits : []
|
||||||
|
if (result && result.hits) {
|
||||||
|
if (instancelocated.length === 0) {
|
||||||
|
await client.index({
|
||||||
|
index: constant.index,
|
||||||
|
body: {
|
||||||
|
instance: server,
|
||||||
|
peers: instancesall,
|
||||||
|
api,
|
||||||
|
nodeinfo,
|
||||||
|
blocks: blocks && blocks.length > 0 ? blocks : [],
|
||||||
|
last: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.locals.created++
|
||||||
|
} else {
|
||||||
|
const elasticinstance = instancelocated[0]._source.peers || []
|
||||||
|
if (Array.isArray(elasticinstance) && Array.isArray(instancesall) && instancesall.length > 0) {
|
||||||
|
if (instancesall.length !== elasticinstance.filter(i => !constant.filterdomains.some(d => i.endsWith(d))).length) {
|
||||||
|
await client.update({
|
||||||
|
index: constant.index,
|
||||||
|
id: instancelocated[0]._id,
|
||||||
|
doc: {
|
||||||
|
peers: instancesall.length > 0 ? instancesall : elasticinstance,
|
||||||
|
api: api ? api : instancelocated[0]._source.api,
|
||||||
|
nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo,
|
||||||
|
blocks: blocks && blocks.length > 0 ? blocks : instancelocated[0]._source.blocks,
|
||||||
|
last: new Date()
|
||||||
|
},
|
||||||
|
doc_as_upsert: true
|
||||||
|
})
|
||||||
|
app.locals.updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanReturn = async () => {
|
||||||
|
app.locals.scannum = 0
|
||||||
|
if (app.locals.servers?.length > 0) {
|
||||||
|
await scan(app.locals.servers.shift())
|
||||||
|
} else {
|
||||||
|
await scan(constant.initialscan)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scan = async server => {
|
||||||
|
if (server) {
|
||||||
|
try {
|
||||||
|
console.log(server)
|
||||||
|
app.locals.scan.emit('data', 'data: ' + server + ' peers...\n\n')
|
||||||
|
const instances = await requestPart(`https://${server}/api/v1/instance/peers`),
|
||||||
|
instancesall = Array.isArray(instances) && instances.length > 0
|
||||||
|
? instances.filter(i => !constant.filterdomains.some(d => i.endsWith(d)))
|
||||||
|
: [],
|
||||||
|
instancessorted = instancesall.sort((a, b) => a < b ? -1 : a > b ? 1 : 0),
|
||||||
|
parts = [],
|
||||||
|
split = workers,
|
||||||
|
chunkSize = Math.ceil(instancessorted.length / split)
|
||||||
|
if (instancesall.length > 0) {
|
||||||
|
await scanIndex(server, instancesall)
|
||||||
|
if (!app.locals.servers || app.locals.servers.length === 0) {
|
||||||
|
app.locals.servers = instancesall.sort(() => Math.random() - 0.5)
|
||||||
|
}
|
||||||
|
if (server === constant.initialscan) {
|
||||||
|
return scan(app.locals.servers.shift())
|
||||||
|
}
|
||||||
|
app.locals.scannum = instancessorted.length
|
||||||
|
app.locals.scantotal = instancessorted.length
|
||||||
|
app.locals.server = server
|
||||||
|
app.locals.instances++
|
||||||
|
for (let i = 0; i < instancessorted.length; i += chunkSize) {
|
||||||
|
const chunk = instancessorted.slice(i, i + chunkSize)
|
||||||
|
parts.push(chunk)
|
||||||
|
}
|
||||||
|
await PromisePool
|
||||||
|
.withConcurrency(workers)
|
||||||
|
.for(parts)
|
||||||
|
.process(scanPart)
|
||||||
|
await scanReturn()
|
||||||
|
} else {
|
||||||
|
await scanReturn()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.error(e)
|
||||||
|
await scanReturn()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await scanReturn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
job = schedule.scheduleJob('0 ' + constant.schedule + ' * * *', async () => {
|
||||||
|
await sendFederatedMessage(constant.nick, null, 'Scanning ' + app.locals.server + ' instance with ' + app.locals.scantotal + ' peers\nScanned ' + app.locals.peers + ' peers from ' + app.locals.instances + ' instances, ' + app.locals.created + ' created, ' + app.locals.updated + ' updated\nhttps://' + constant.apexdomain)
|
||||||
|
})
|
||||||
|
return scanReturn()
|
||||||
|
}
|
||||||
6
back/lib/logger.js
Archivo normal
6
back/lib/logger.js
Archivo normal
@@ -0,0 +1,6 @@
|
|||||||
|
const morgan = require('morgan'),
|
||||||
|
rfs = require('rotating-file-stream'),
|
||||||
|
accessLogStream = rfs.createStream('access.log', { interval: '1d', path: __dirname + '/../logs' }),
|
||||||
|
logger = morgan('combined', { stream: accessLogStream })
|
||||||
|
|
||||||
|
module.exports = logger
|
||||||
25
back/lib/swagger.js
Archivo normal
25
back/lib/swagger.js
Archivo normal
@@ -0,0 +1,25 @@
|
|||||||
|
module.exports = app => {
|
||||||
|
const swaggerJsdoc = require('swagger-jsdoc'),
|
||||||
|
swaggerUi = require('swagger-ui-express'),
|
||||||
|
options = {
|
||||||
|
swaggerOptions: {
|
||||||
|
withCredentials: false
|
||||||
|
},
|
||||||
|
swaggerDefinition: {
|
||||||
|
restapi: '3.1.0',
|
||||||
|
info: {
|
||||||
|
title: 'Fediblock Instance API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '#Fediblock Instance REST API',
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
apis: ['**/apiswagger.js'],
|
||||||
|
},
|
||||||
|
specs = swaggerJsdoc(options)
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, { explorer: false }))
|
||||||
|
}
|
||||||
69
back/lib/taskdeletedup.js
Archivo normal
69
back/lib/taskdeletedup.js
Archivo normal
@@ -0,0 +1,69 @@
|
|||||||
|
module.exports = client => {
|
||||||
|
const constant = require('../constant'),
|
||||||
|
schedule = require('node-schedule'),
|
||||||
|
{ pick } = require('stream-json/filters/Pick'),
|
||||||
|
{ parser } = require('stream-json'),
|
||||||
|
{ streamArray } = require('stream-json/streamers/StreamArray'),
|
||||||
|
{ chain } = require('stream-chain'),
|
||||||
|
size = 500,
|
||||||
|
deleteDup = async () => {
|
||||||
|
const count = { deleted: 0, total: 0, current: 0 }
|
||||||
|
let lastsort = undefined, last = undefined
|
||||||
|
await searchDup(count, lastsort, last)
|
||||||
|
},
|
||||||
|
searchDup = async (count, lastsort, last) => {
|
||||||
|
count.current = 0
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: size,
|
||||||
|
body: {
|
||||||
|
query: {
|
||||||
|
match_all: {}
|
||||||
|
},
|
||||||
|
sort: [{
|
||||||
|
"instance": {
|
||||||
|
"order": "asc"
|
||||||
|
},
|
||||||
|
"last": {
|
||||||
|
"order": "desc",
|
||||||
|
"numeric_type": "date_nanos",
|
||||||
|
"format": "strict_date_optional_time_nanos"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
search_after: lastsort
|
||||||
|
}
|
||||||
|
}, { asStream: true, meta: false }),
|
||||||
|
pipeline = chain([
|
||||||
|
parser(),
|
||||||
|
pick({ filter: 'hits.hits' }),
|
||||||
|
streamArray(),
|
||||||
|
data => data.value
|
||||||
|
])
|
||||||
|
pipeline.on('data', async data => {
|
||||||
|
count.current++
|
||||||
|
if (last && last.instance === data._source.instance) {
|
||||||
|
await client.delete({ index: constant.index, id: data._id })
|
||||||
|
count.deleted++
|
||||||
|
console.log('deleted ' + data._id + ': ' + data._source.instance)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
last = data._source
|
||||||
|
}
|
||||||
|
if (count.current === size) {
|
||||||
|
lastsort = data.sort
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pipeline.on('end', async () => {
|
||||||
|
count.total += count.current
|
||||||
|
if (count.current === size) {
|
||||||
|
await searchDup(count, lastsort, last)
|
||||||
|
} else {
|
||||||
|
console.log('Index: ' + constant.index + ' - Total: ' + count.total + ' - Deleted: ' + count.deleted)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.pipe(pipeline)
|
||||||
|
},
|
||||||
|
job = schedule.scheduleJob('0 ' + constant.taskdeletedup + ' * * *', async () => {
|
||||||
|
await deleteDup()
|
||||||
|
})
|
||||||
|
}
|
||||||
83
back/lib/util.js
Archivo normal
83
back/lib/util.js
Archivo normal
@@ -0,0 +1,83 @@
|
|||||||
|
const constant = require("../constant"),
|
||||||
|
{ apex } = require('./apex'),
|
||||||
|
https = require('node:https'),
|
||||||
|
json = 'application/json',
|
||||||
|
urlToId = url => {
|
||||||
|
try {
|
||||||
|
if (typeof new URL(url) === 'object' && url.split('/').length === 5 && url.split('/')[3].length > 0) {
|
||||||
|
return '@' + url.split('/')[4] + '@' + url.split('/')[2]
|
||||||
|
} else {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendFederatedMessage = async (id, summary, message, recipient, reply) => {
|
||||||
|
const users = message.split(' ').filter(token => token.startsWith('@') && token.split('@').length === 3)
|
||||||
|
.map(token => `https://${token.split('@')[2]}/users/${token.split('@')[1]}`),
|
||||||
|
mentions = message.split(' ').filter(token => token.startsWith('@') && token.split('@').length === 3)
|
||||||
|
.map(token => { return { type: 'Mention', href: `https://${token.split('@')[2]}/users/${token.split('@')[1]}`, name: `@${token.split('@')[1]}` } }),
|
||||||
|
hashtags = message.split(' ').filter(token => token.startsWith('#'))
|
||||||
|
.map(token => { return { id: `https://${constant.apexdomain}/tags/${token.split('#')[1]}`, name: token } }),
|
||||||
|
images = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(jpg|png|gif|jpeg|ppm)$/i))
|
||||||
|
.map(token => { return { type: 'Image', url: token } }),
|
||||||
|
audio = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(mp3|wav|flac)$/i))
|
||||||
|
.map(token => { return { type: 'Audio', url: token } }),
|
||||||
|
video = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(mp4|flv|avi)$/i))
|
||||||
|
.map(token => { return { type: 'Video', url: token } }),
|
||||||
|
links = message.split(' ').filter(token => token.match(/^https?:\/\//i) && !images.concat(audio).concat(video)
|
||||||
|
.some(link => link.url === token)).map(token => { return { type: 'Link', href: token } }),
|
||||||
|
allusers = users.concat([recipient || (await apex.store.getObject(apex.utils.usernameToIRI(id), true)).followers[0]]),
|
||||||
|
act = await apex.buildActivity('Create', apex.utils.usernameToIRI(id), ['https://www.w3.org/ns/activitystreams#Public'].concat(allusers), {
|
||||||
|
object: {
|
||||||
|
type: 'Note',
|
||||||
|
name: `Fediblock Instance`,
|
||||||
|
summary: summary || null,
|
||||||
|
content: `<p>${message.replace(/(\b(https?|ftp|file):\/\/([-A-Z0-9+&@#%?=~_|!:,.;]*)([-A-Z0-9+&@#%?/=~_|!:,.;]*))/ig,
|
||||||
|
'<a href="$1" target="_blank">$3</a>').replace(/\s#(\S+)/g, '<a href="https://mastodon.social/tags/$1">#$1</a>').replace(/\r?\n/g, '<br />')}</p>`,
|
||||||
|
tag: hashtags.concat(mentions),
|
||||||
|
attachment: images.concat(links).concat(audio).concat(video),
|
||||||
|
inReplyTo: reply || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
act.object[0].id = act.id
|
||||||
|
await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(id), true), act)
|
||||||
|
},
|
||||||
|
requestPart = uri => new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const data = [],
|
||||||
|
req = https.request(uri, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': constant.agent,
|
||||||
|
'Content-Type': json
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(constant.abort_timeout),
|
||||||
|
}, res => {
|
||||||
|
res.setEncoding('utf8')
|
||||||
|
res.headers['Content-Type'] = json
|
||||||
|
res.on('data', chunk => {
|
||||||
|
data.push(chunk)
|
||||||
|
})
|
||||||
|
res.on('error', error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data.join('')))
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
req.on('error', error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
req.end()
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = { urlToId, sendFederatedMessage, requestPart }
|
||||||
|
|
||||||
35
back/package.json
Archivo normal
35
back/package.json
Archivo normal
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "fediblock-instance",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Fediblock Instance",
|
||||||
|
"author": "ale",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.manalejandro.com/ale/fediblock-instance"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"build": "cd ../front && yarn build",
|
||||||
|
"install": "cd node_modules && rm -rf http-signature && rm -rf request/node_modules/http-signature && mv @peertube/http-signature . && cd ../../front && yarn"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@elastic/elasticsearch": "^8.17.1",
|
||||||
|
"@peertube/http-signature": "^1.7.0",
|
||||||
|
"@supercharge/promise-pool": "^3.2.0",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"activitypub-express": "^4.4.2",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"express-async-handler": "^1.2.0",
|
||||||
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"express-validator": "^7.2.1",
|
||||||
|
"mongodb": "^4.17.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"rotating-file-stream": "^3.2.6",
|
||||||
|
"stream-json": "^1.9.1",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
back/served.txt
Archivo normal
1
back/served.txt
Archivo normal
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
55
docker-compose.yml
Archivo normal
55
docker-compose.yml
Archivo normal
@@ -0,0 +1,55 @@
|
|||||||
|
services:
|
||||||
|
fediblock-instance:
|
||||||
|
image: fediblock-instance
|
||||||
|
build: .
|
||||||
|
hostname: fediblock-instance
|
||||||
|
container_name: fediblock-instance
|
||||||
|
restart: always
|
||||||
|
user: node
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
depends_on:
|
||||||
|
- fediblock-mongodb
|
||||||
|
- fediblock-elasticsearch
|
||||||
|
networks:
|
||||||
|
fediblocknet:
|
||||||
|
|
||||||
|
fediblock-mongodb:
|
||||||
|
image: mongo:4.4.28
|
||||||
|
hostname: fediblock-mongodb
|
||||||
|
container_name: fediblock-mongodb
|
||||||
|
restart: always
|
||||||
|
command: --wiredTigerCacheSizeGB 0.5
|
||||||
|
volumes:
|
||||||
|
- ./mongodb:/data/db
|
||||||
|
networks:
|
||||||
|
fediblocknet:
|
||||||
|
|
||||||
|
fediblock-elasticsearch:
|
||||||
|
image: docker.elastic.co/elasticsearch/elasticsearch:8.17.3-amd64
|
||||||
|
hostname: fediblock-elasticsearch
|
||||||
|
container_name: fediblock-elasticsearch
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- node.name=fediblock-elasticsearch
|
||||||
|
- bootstrap.memory_lock=true
|
||||||
|
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
|
||||||
|
- xpack.security.enabled=false
|
||||||
|
- indices.id_field_data.enabled=true
|
||||||
|
- discovery.type=single-node
|
||||||
|
ulimits:
|
||||||
|
memlock:
|
||||||
|
soft: -1
|
||||||
|
hard: -1
|
||||||
|
nofile:
|
||||||
|
soft: 65535
|
||||||
|
hard: 65535
|
||||||
|
volumes:
|
||||||
|
- ./elasticsearch-data:/usr/share/elasticsearch/data
|
||||||
|
expose:
|
||||||
|
- 9200
|
||||||
|
networks:
|
||||||
|
fediblocknet:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
fediblocknet:
|
||||||
23
front/.gitignore
vendido
Archivo normal
23
front/.gitignore
vendido
Archivo normal
@@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
70
front/README.md
Archivo normal
70
front/README.md
Archivo normal
@@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
38
front/package.json
Archivo normal
38
front/package.json
Archivo normal
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "front",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cra-template": "1.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"web-vitals": "^4.2.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
front/public/favicon.ico
Archivo normal
BIN
front/public/favicon.ico
Archivo normal
Archivo binario no mostrado.
|
Después Anchura: | Altura: | Tamaño: 30 KiB |
21
front/public/index.html
Archivo normal
21
front/public/index.html
Archivo normal
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Fediblock Instance Φ</title>
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Fediblock Instance Φ - Search in public instances - API docs">
|
||||||
|
<meta name="keywords" content="Fediblock, Fediblock Instance, Fediblock Instance Φ, Free, FreeSoftware">
|
||||||
|
<meta property="og:title" content="Fediblock Instance Φ">
|
||||||
|
<meta property="og:type" content="service">
|
||||||
|
<meta property="og:image" content="favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
front/public/logo192.png
Archivo normal
BIN
front/public/logo192.png
Archivo normal
Archivo binario no mostrado.
|
Después Anchura: | Altura: | Tamaño: 5.2 KiB |
BIN
front/public/logo512.png
Archivo normal
BIN
front/public/logo512.png
Archivo normal
Archivo binario no mostrado.
|
Después Anchura: | Altura: | Tamaño: 9.4 KiB |
15
front/public/manifest.json
Archivo normal
15
front/public/manifest.json
Archivo normal
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"short_name": "New Fediblock Instance",
|
||||||
|
"name": "New Fediblock Instance Front",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "78x90 72x72",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
front/public/robots.txt
Archivo normal
3
front/public/robots.txt
Archivo normal
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
405
front/src/App.css
Archivo normal
405
front/src/App.css
Archivo normal
@@ -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
28
front/src/App.js
Archivo normal
@@ -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
8
front/src/App.test.js
Archivo normal
@@ -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
29
front/src/component/Bar.js
Archivo normal
@@ -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
41
front/src/component/Count.js
Archivo normal
@@ -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
67
front/src/component/Footer.js
Archivo normal
@@ -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>©</s>2025
|
||||||
|
<a className="darklight" onClick={() => toggleTheme()}>{!theme || theme === 'dark' ? '☼' : '☽'}</a>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
167
front/src/component/Form.js
Archivo normal
167
front/src/component/Form.js
Archivo normal
@@ -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 ` : ` ` }}></span>
|
||||||
|
<span dangerouslySetInnerHTML={{
|
||||||
|
__html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">ⓘ</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
19
front/src/component/Loader.js
Archivo normal
@@ -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
142
front/src/component/Modal.js
Archivo normal
@@ -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 ? ` <a href="/api/detail_nodeinfo/${res.instance}" title="Nodeinfo for ${res.instance}" target="_blank">ⓘ</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)}>×</span>
|
||||||
|
<a className="capture" title="Take snapshot" onClick={capture} type="image/png" target="_blank">📷</a>
|
||||||
|
{!prop.reverse ? <a className="download" title="Download fediblock CSV mastodon file" onClick={download} type="text/csv" target="_blank">⇩</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
31
front/src/component/Scan.js
Archivo normal
@@ -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
13
front/src/component/Title.js
Archivo normal
@@ -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 Φ</a></h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Title;
|
||||||
12
front/src/index.js
Archivo normal
12
front/src/index.js
Archivo normal
@@ -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
411
front/src/loaders.css
Archivo normal
@@ -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
1
front/src/logo.svg
Archivo normal
@@ -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
69
front/src/random-text.js
Archivo normal
@@ -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
13
front/src/reportWebVitals.js
Archivo normal
@@ -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
5
front/src/setupTests.js
Archivo normal
@@ -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';
|
||||||
Referencia en una nueva incidencia
Block a user