This commit is contained in:
commit
e0fae7a6b4
40
.drone.yml
Normal file
40
.drone.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
kind: pipeline
|
||||||
|
name: 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
|
||||||
|
- name: usrbin
|
||||||
|
path: /usr/bin
|
||||||
|
commands:
|
||||||
|
- docker login -u $USER -p $PASS $REGISTRY
|
||||||
|
- docker buildx build --platform amd64 -t $REGISTRY/fediblock-instance .
|
||||||
|
- docker push $REGISTRY/fediblock-instance
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- tag
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
- name: usrbin
|
||||||
|
host:
|
||||||
|
path: /usr/bin
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.parcel-cache/
|
||||||
|
logs/
|
||||||
|
dist/
|
||||||
|
yarn.lock
|
14
.gitlab-ci.yml
Normal file
14
.gitlab-ci.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
image: docker:dind
|
||||||
|
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- docker -v
|
||||||
|
|
||||||
|
build image:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
- docker build -t $CI_REGISTRY/manalejandro/fediblock-instance/fediblock-instance:latest .
|
||||||
|
- docker push $CI_REGISTRY/manalejandro/fediblock-instance/fediblock-instance:latest
|
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM node:21-slim
|
||||||
|
RUN apt update && apt install -y git && apt clean
|
||||||
|
COPY --chown=node:node . /fediblock-instance
|
||||||
|
USER node
|
||||||
|
WORKDIR /fediblock-instance
|
||||||
|
RUN yarn config set network-timeout 300000 && yarn && yarn build
|
||||||
|
EXPOSE 4000
|
||||||
|
ENTRYPOINT ["yarn", "start"]
|
42
README.md
Normal file
42
README.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Fediblock Instance [![Build Status](https://drone.manalejandro.com/api/badges/ale/fediblock-instance/status.svg)](https://drone.manalejandro.com/ale/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 `lib/constant.js` with your environment preferences
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start or npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker compose build or docker pull registry.gitlab.com/manalejandro/fediblock-instance/fediblock-instance:latest
|
||||||
|
$ 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
|
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
fediblock-instance:
|
||||||
|
# image: registry.gitlab.com/manalejandro/fediblock-instance/fediblock-instance:latest
|
||||||
|
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.13.2-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:
|
0
elasticsearch-data/.git-keep
Normal file
0
elasticsearch-data/.git-keep
Normal file
1799
fediblock-mapping.json
Normal file
1799
fediblock-mapping.json
Normal file
File diff suppressed because it is too large
Load Diff
21
lib/apex.js
Normal file
21
lib/apex.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// define routes using prepacakged middleware collections
|
||||||
|
module.exports = (app, apex, routes) => {
|
||||||
|
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)
|
||||||
|
}
|
105
lib/apexcustom.js
Normal file
105
lib/apexcustom.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// custom side-effects for your app
|
||||||
|
module.exports = (app, apex, client) => {
|
||||||
|
const util = require('./util')(apex),
|
||||||
|
constant = require('./constant'),
|
||||||
|
https = require('https')
|
||||||
|
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') {
|
||||||
|
const follow = 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) {
|
||||||
|
const instance = content.join('').trim(),
|
||||||
|
ac = new AbortController()
|
||||||
|
try {
|
||||||
|
setTimeout(() => {
|
||||||
|
ac.abort()
|
||||||
|
}, 5000)
|
||||||
|
const response = await fetch('https://' + constant.apexdomain + '/api/detail/' + instance, {
|
||||||
|
headers: { 'User-Agent': constant.agent },
|
||||||
|
agent: new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
keepAlive: false
|
||||||
|
}),
|
||||||
|
signal: ac.signal
|
||||||
|
}),
|
||||||
|
json = await response.json()
|
||||||
|
if (json && json.blocks && Array.isArray(json.blocks) && json.blocks.length > 0) {
|
||||||
|
const ac2 = new AbortController()
|
||||||
|
setTimeout(() => {
|
||||||
|
ac2.abort()
|
||||||
|
}, 5000)
|
||||||
|
const result = await fetch('https://' + constant.apexdomain + '/api/list/' + instance, {
|
||||||
|
headers: { 'User-Agent': constant.agent },
|
||||||
|
agent: new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
keepAlive: false
|
||||||
|
}),
|
||||||
|
signal: ac2.signal
|
||||||
|
}),
|
||||||
|
res = await result.json()
|
||||||
|
if (res && res.instances && Array.isArray(res.instances)) {
|
||||||
|
res.instances.map(async r => {
|
||||||
|
if (r.domain === instance) {
|
||||||
|
await util.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 util.sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks - https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await util.sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' not found, next try.', msg.actor.id, msg.object.id)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await util.sendFederatedMessage(constant.nick, null, e.message, msg.actor.id, msg.object.id)
|
||||||
|
// console.error(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await util.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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
53
lib/api.js
Normal file
53
lib/api.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
module.exports = (app, apex) => {
|
||||||
|
const constant = require('./constant'),
|
||||||
|
{ readFile, writeFile } = require('fs')
|
||||||
|
app.use('/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.use('/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.use('/api/outbox', async (req, res) => {
|
||||||
|
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.use('/api/inbox', async (req, res) => {
|
||||||
|
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').map(e => e.object[0].content[0])
|
||||||
|
res.json(notes)
|
||||||
|
})
|
||||||
|
}
|
489
lib/apiswagger.js
Normal file
489
lib/apiswagger.js
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
const nodeinfo = require('activitypub-express/pub/nodeinfo')
|
||||||
|
|
||||||
|
module.exports = (app, client) => {
|
||||||
|
const constant = require('./constant'),
|
||||||
|
clean = str => {
|
||||||
|
return str.replace(/[\/\\^$+?()`'¡¿¨!"·%&=;,\|\[\]{}]+/gmi, '')
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @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.use('/api/stats', 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.use('/api/count', 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.use('/api/ranking', 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.use('/api/list/:instance', async (req, res) => {
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 10,
|
||||||
|
query: {
|
||||||
|
wildcard: {
|
||||||
|
instance: {
|
||||||
|
value: `*${clean(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 && result.hits.hits && 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.use('/api/detail/:instance', async (req, res) => {
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: clean(req.params.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits && result.hits.hits && 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.use('/api/detail_api/:instance', async (req, res) => {
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: clean(req.params.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits && result.hits.hits && 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.use('/api/detail_nodeinfo/:instance', async (req, res) => {
|
||||||
|
if (req.params.instance && req.params.instance.length > 0) {
|
||||||
|
const result = await client.search({
|
||||||
|
index: constant.index,
|
||||||
|
size: 1,
|
||||||
|
query: {
|
||||||
|
term: {
|
||||||
|
instance: clean(req.params.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits && result.hits.hits && 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.use('/api/block_count/:instance', async (req, res) => {
|
||||||
|
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': clean(req.params.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const instances = result.hits && result.hits.hits && result.hits.hits.length > 0 ? result.hits.hits : [],
|
||||||
|
instancescomment = instances.map(i => ({
|
||||||
|
instance: i._source.instance, comment: i._source.blocks.find(block => block.domain === clean(req.params.instance)).comment
|
||||||
|
}))
|
||||||
|
global.gc()
|
||||||
|
res.json({
|
||||||
|
block_count: instances.length,
|
||||||
|
instances: instancescomment,
|
||||||
|
took: result.took
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.status(404).end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
14
lib/constant.js
Normal file
14
lib/constant.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
|
schedule: '5,11,17,23', // UTC hours to publish bot followers federated stats
|
||||||
|
filterdomains: ['activitypub-troll.cf'], // domains filtered to scan
|
||||||
|
initialscan: 'mastodon.social' // initial federated domain to scan
|
||||||
|
}
|
261
lib/fediblock.js
Normal file
261
lib/fediblock.js
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
let servers
|
||||||
|
module.exports = async (client, apex, app) => {
|
||||||
|
const util = require('./util')(apex),
|
||||||
|
constant = require('./constant'),
|
||||||
|
https = require('https'),
|
||||||
|
schedule = require('node-schedule'),
|
||||||
|
workers = constant.workers,
|
||||||
|
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 ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestPart = async uri => {
|
||||||
|
const ac = new AbortController()
|
||||||
|
try {
|
||||||
|
setTimeout(() => {
|
||||||
|
ac.abort()
|
||||||
|
}, 5000)
|
||||||
|
const response = await fetch(uri, {
|
||||||
|
headers: { 'User-Agent': constant.agent },
|
||||||
|
agent: new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
keepAlive: false
|
||||||
|
}),
|
||||||
|
signal: ac.signal,
|
||||||
|
keepalive: false,
|
||||||
|
timeout: 4000
|
||||||
|
}),
|
||||||
|
json = await response.json()
|
||||||
|
setImmediate(() => { ac.abort() })
|
||||||
|
return json
|
||||||
|
} catch (e) {
|
||||||
|
setImmediate(() => { ac.abort() })
|
||||||
|
// console.error(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanInstance = async instance => {
|
||||||
|
try {
|
||||||
|
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 (instancelocated.length === 0) {
|
||||||
|
await client.index({
|
||||||
|
index: constant.index,
|
||||||
|
body: {
|
||||||
|
instance,
|
||||||
|
api,
|
||||||
|
nodeinfo,
|
||||||
|
blocks,
|
||||||
|
peers,
|
||||||
|
last: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.locals.created++
|
||||||
|
return await util.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 < 50) {
|
||||||
|
return await util.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))
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanPart = async (instancesall, index) => {
|
||||||
|
for (const instance of instancesall) {
|
||||||
|
try {
|
||||||
|
app.locals.scan.emit('data', 'data: ' + (app.locals.scannum > 0 ? '(' + app.locals.scannum-- + ':' + (index + 1) + '): ' + instance : ': ' + instance) + '\n\n')
|
||||||
|
app.locals.peers++
|
||||||
|
await scanInstance(instance)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.gc()
|
||||||
|
return
|
||||||
|
},
|
||||||
|
scanIndex = async (server, instancesall) => {
|
||||||
|
try {
|
||||||
|
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 (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++
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanReturn = async () => {
|
||||||
|
global.gc()
|
||||||
|
app.locals.scannum = 0
|
||||||
|
if (servers && servers.length > 0) {
|
||||||
|
return await scan(servers.shift())
|
||||||
|
} else {
|
||||||
|
return 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.floor(instancessorted.length / split)
|
||||||
|
await scanIndex(server, instancesall)
|
||||||
|
if (instancesall.length > 0) {
|
||||||
|
if (!servers || servers.length === 0) {
|
||||||
|
servers = instancesall.sort(() => Math.random() - 0.5)
|
||||||
|
}
|
||||||
|
if (server && server === constant.initialscan) {
|
||||||
|
return await scan(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 Promise.all(parts.map(async (p, i) => await scanPart(p, i)))
|
||||||
|
return await scanReturn()
|
||||||
|
} else {
|
||||||
|
return await scanReturn()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return await scanReturn()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await scanReturn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
job = schedule.scheduleJob('0 ' + constant.schedule + ' * * *', async () => {
|
||||||
|
return await util.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 await scanReturn()
|
||||||
|
}
|
7
lib/logger.js
Normal file
7
lib/logger.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = app => {
|
||||||
|
const morgan = require('morgan'),
|
||||||
|
rfs = require('rotating-file-stream'),
|
||||||
|
accessLogStream = rfs.createStream('access.log', { interval: '1d', path: __dirname + '/../logs' }),
|
||||||
|
logger = morgan('combined', { stream: accessLogStream })
|
||||||
|
app.use(logger)
|
||||||
|
}
|
25
lib/swagger.js
Normal file
25
lib/swagger.js
Normal file
@ -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 }))
|
||||||
|
}
|
45
lib/util.js
Normal file
45
lib/util.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
module.exports = apex => {
|
||||||
|
const 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://mastodon.social/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 ? 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 ? 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(new RegExp('\r?\n', 'g'), '<br />')}</p>`,
|
||||||
|
tag: hashtags.concat(mentions),
|
||||||
|
attachment: images.concat(links).concat(audio).concat(video),
|
||||||
|
inReplyTo: reply ? reply : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
act.object[0].id = act.id
|
||||||
|
await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(id), true), act)
|
||||||
|
}
|
||||||
|
return { urlToId, sendFederatedMessage }
|
||||||
|
}
|
106
lib/worker-fediblock.js
Normal file
106
lib/worker-fediblock.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
module.exports = async (client, apex, app) => {
|
||||||
|
util = require('./util')(apex),
|
||||||
|
constant = require('./constant'),
|
||||||
|
{ parentPort } = require('worker_threads'),
|
||||||
|
scanInstance = async instance => {
|
||||||
|
try {
|
||||||
|
const json = await util.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([
|
||||||
|
util.requestPart(`https://${instance}/api/v1/instance`),
|
||||||
|
util.requestPart(`https://${instance}/nodeinfo/2.0`),
|
||||||
|
util.requestPart(`https://${instance}/api/v1/instance/peers`)
|
||||||
|
])
|
||||||
|
if (instancelocated.length === 0) {
|
||||||
|
await client.index({
|
||||||
|
index: constant.index,
|
||||||
|
body: {
|
||||||
|
instance,
|
||||||
|
api,
|
||||||
|
nodeinfo,
|
||||||
|
blocks,
|
||||||
|
peers,
|
||||||
|
last: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.locals.created++
|
||||||
|
return await util.sendFederatedMessage(constant.nick, 'New Fediblock Instance', `Fediblock Instance ${instance} with ${json.length} blocks - https://${constant.apexdomain}#${instance}`, util.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 < 50) {
|
||||||
|
return await util.sendFederatedMessage(constant.nick, 'Detected #Fediblock by Fediblock Instance', `You blocked new instances: ${difference.map(d => d.domain).join(', ')} - https://${constant.apexdomain}#${instance}`, util.getAccount(instancelocated[0]._source.api))
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scanPart = async (instancesall, index) => {
|
||||||
|
for (const instance of instancesall) {
|
||||||
|
try {
|
||||||
|
app.locals.scan.emit('data', 'data: ' + (app.locals.scannum > 0 ? '(' + app.locals.scannum-- + ':' + (index + 1) + '): ' + instance : ': ' + instance) + '\n\n')
|
||||||
|
app.locals.peers++
|
||||||
|
await scanInstance(instance)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
},
|
||||||
|
work = (part, index) => {
|
||||||
|
return parentPort.postMessage({ func: scanPart, args: [part, index] })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "fediblock-instance",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Fediblock Instance",
|
||||||
|
"author": "ale",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitlab.com/manalejandro/fediblock-instance"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node --max-old-space-size=1024 --expose-gc server.js",
|
||||||
|
"build": "rm -rf dist/* && parcel build public/index.html --no-source-maps --dist-dir dist/",
|
||||||
|
"install": "cd node_modules && rm -rf http-signature && rm -rf request/node_modules/http-signature && mv @peertube/http-signature ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@elastic/elasticsearch": "^8.14.0",
|
||||||
|
"@peertube/http-signature": "^1.7.0",
|
||||||
|
"activitypub-express": "^4.4.2",
|
||||||
|
"dayjs": "^1.11.12",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"mongodb": "^4.17.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"parcel": "^2.12.0",
|
||||||
|
"rotating-file-stream": "^3.2.3",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
77
public/index.html
Normal file
77
public/index.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
||||||
|
<title>Fediblock Instance Φ</title>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<meta name="description" content="Fediblock Instance Φ - Search in public instances - API docs - Git">
|
||||||
|
<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='stylesheet' type='text/css' media='screen' href='main.css'>
|
||||||
|
<link rel='stylesheet' type='text/css' media='screen' href='loaders.css'>
|
||||||
|
<script src='random-text.js'></script>
|
||||||
|
<script src='../node_modules/html2canvas/dist/html2canvas.min.js'></script>
|
||||||
|
<script src='../node_modules/dayjs/dayjs.min.js'></script>
|
||||||
|
<script type="module">
|
||||||
|
import relativeTime from '../node_modules/dayjs/plugin/relativeTime'
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
</script>
|
||||||
|
<script src='main.js'></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1><a href="/" id="title">Fediblock Instance Φ</a></h1>
|
||||||
|
<h4>
|
||||||
|
<p id="bounce"> </p>
|
||||||
|
</h4>
|
||||||
|
<h3>Search in <a href="/api/stats" target="_blank"><span id="count"></span></a> public instances<span id="apigit"> -
|
||||||
|
<a href="api-docs" target="_blank">API docs</a> - <a
|
||||||
|
href="https://gitlab.com/manalejandro/fediblock-instance" target="_blank">Git</a></span></h3>
|
||||||
|
<section>
|
||||||
|
<input type="text" id="instance" name="instance" placeholder="Type the name of the instance" />
|
||||||
|
<button id="reverse" title="Reverse search...">Reverse</button>
|
||||||
|
<div id="placeholder"></div>
|
||||||
|
<div id="loader-content">
|
||||||
|
<div id="loader">
|
||||||
|
<div id="load"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul id="instancelist"></ul>
|
||||||
|
</section>
|
||||||
|
<div>
|
||||||
|
<span id="scan"></span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<footer>Served <span id="served">0</span> times - Last scan <span id="lastscan">0</span> peers of <span
|
||||||
|
id="server"></span><br />
|
||||||
|
Total scanned <span id="instances">0</span> instances with <span id="peers">0</span> peers - <span
|
||||||
|
id="created">0</span> created - <span id="updated">0</span>
|
||||||
|
updated<br />
|
||||||
|
by <a href="https://about.manalejandro.com" target="_blank">ale</a> ©2024 - <a id="matrix"
|
||||||
|
href="/?matrix">matrix off</a></footer>
|
||||||
|
<div id="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span id="closemodal" title="Close">×</span>
|
||||||
|
<a id="capture" title="Take snapshot">📷</a>
|
||||||
|
<a id="download" title="Download fediblock CSV mastodon file">⇩</a>
|
||||||
|
<h2>Blocked List</h2>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
<span id="blockcount"></span> public instances are block<span id="blockinstance"></span>
|
||||||
|
<br /><small>(search in <span id="blocktook"></span>ms)</small>
|
||||||
|
</p>
|
||||||
|
<ul id="blocklist"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
413
public/loaders.css
Normal file
413
public/loaders.css
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
/* 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%);
|
||||||
|
}
|
||||||
|
}
|
378
public/main.css
Normal file
378
public/main.css
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
body {
|
||||||
|
background-color: #212529;
|
||||||
|
text-align: center;
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
#scan {
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #fefefe;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:hover {
|
||||||
|
color: #fefefe;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
background-color: #262c33;
|
||||||
|
color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
#placeholder {
|
||||||
|
font-size: small;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem auto;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tooltip {
|
||||||
|
visibility: hidden;
|
||||||
|
font-size: small;
|
||||||
|
text-align: left;
|
||||||
|
width: 2.2in;
|
||||||
|
background-color: #212529;
|
||||||
|
color: #fefefe;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1rem 1rem 0 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.5s;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#count:hover #tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reverse {
|
||||||
|
margin: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #fefefe;
|
||||||
|
color: #262c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reverse:hover {
|
||||||
|
color: #fefefe;
|
||||||
|
background-color: #262c33;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instancelist {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #fefefe;
|
||||||
|
max-height: 58vh;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
#instancelist li {
|
||||||
|
width: 28%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instancelist li:hover {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instance,
|
||||||
|
#placeholder,
|
||||||
|
h4 {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) and (max-width: 992px) {
|
||||||
|
#instancelist li {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instancelist li:hover {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instance,
|
||||||
|
#placeholder,
|
||||||
|
h4 {
|
||||||
|
width: 63%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#instancelist li {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instancelist li:hover {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instance,
|
||||||
|
#placeholder,
|
||||||
|
h4 {
|
||||||
|
width: 83%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#apigit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#instancelist li {
|
||||||
|
color: #fefefe;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-height: 1.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 1.5em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#instancelist li:hover {
|
||||||
|
background-color: #333;
|
||||||
|
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: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#count {
|
||||||
|
text-decoration: underline dotted #fefefe;
|
||||||
|
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: #262c33;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blocklist li:hover {
|
||||||
|
white-space: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blocklist li a,
|
||||||
|
#blocklist li a:hover,
|
||||||
|
#blocklist li a:visited {
|
||||||
|
color: #262c33;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blockinstance a,
|
||||||
|
#blockinstance a:hover {
|
||||||
|
color: #262c33;
|
||||||
|
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: #fefefe;
|
||||||
|
margin: 10px auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 25%;
|
||||||
|
-webkit-animation-name: animatetop;
|
||||||
|
-webkit-animation-duration: 0.4s;
|
||||||
|
animation-name: animatetop;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.modal-content {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #212529;
|
||||||
|
color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #212529;
|
||||||
|
color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
#closemodal,
|
||||||
|
#download,
|
||||||
|
#capture {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bolder;
|
||||||
|
margin: 0px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#closemodal:hover,
|
||||||
|
#closemodal:focus,
|
||||||
|
#download:hover,
|
||||||
|
#download:focus,
|
||||||
|
#capture:hover,
|
||||||
|
#capture:focus {
|
||||||
|
color: #fefefe;
|
||||||
|
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 #fefefe;
|
||||||
|
border-right: 1px solid #fefefe;
|
||||||
|
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: 1;
|
||||||
|
background-color: #0006;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader {
|
||||||
|
background-color: #fefefe;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 50vh auto;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
418
public/main.js
Normal file
418
public/main.js
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
loading()
|
||||||
|
var hold = false,
|
||||||
|
external = false,
|
||||||
|
timeout = undefined,
|
||||||
|
ac = undefined,
|
||||||
|
last = undefined
|
||||||
|
document.getElementById('instance').addEventListener('keydown', function (event) {
|
||||||
|
if (event.key && ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && event.target.value !== ''))) {
|
||||||
|
last = event.target.value
|
||||||
|
} else if (event.key === 'Backspace' && event.target.value === '') {
|
||||||
|
last = ''
|
||||||
|
} else {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
document.getElementById('instance').addEventListener('keyup', function (event) {
|
||||||
|
if (event.key && ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && last !== ''))) {
|
||||||
|
keypress(event, event.target.value)
|
||||||
|
} else {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var modal = document.getElementById('modal'),
|
||||||
|
closemodal = document.getElementById('closemodal'),
|
||||||
|
modalcontent = document.querySelector('.modal-content')
|
||||||
|
document.getElementById('reverse').addEventListener('click', function (event) {
|
||||||
|
var content = document.getElementById('instance').value
|
||||||
|
if (content && content.length > 0) {
|
||||||
|
loading()
|
||||||
|
fetch('/api/block_count/' + content).then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (res && res.block_count >= 0) {
|
||||||
|
document.getElementById('blockcount').innerText = res.block_count
|
||||||
|
document.getElementById('blockinstance').innerText = 'ing ' + content
|
||||||
|
document.getElementById('blocktook').innerText = res.took
|
||||||
|
var list = document.getElementById('blocklist'),
|
||||||
|
download = document.getElementById('download')
|
||||||
|
download.removeAttribute('href')
|
||||||
|
download.removeAttribute('download')
|
||||||
|
download.style.display = 'none'
|
||||||
|
while (list.hasChildNodes()) {
|
||||||
|
list.removeChild(list.firstChild)
|
||||||
|
}
|
||||||
|
res.instances.map((instance, index) => {
|
||||||
|
var li = document.createElement('li'),
|
||||||
|
text = document.createTextNode((index + 1) + '. '),
|
||||||
|
link = '<a href="/' + (new URLSearchParams(window.location.search).has('matrix') ? '?matrix' : '') + '#' + instance.instance + '" onclick="window.location.href=this.href; window.location.reload(false);">' + instance.instance + '</a>',
|
||||||
|
text2 = document.createTextNode(instance.comment ? ' - ' + instance.comment : '')
|
||||||
|
li.appendChild(text)
|
||||||
|
li.insertAdjacentHTML('beforeend', link)
|
||||||
|
li.appendChild(text2)
|
||||||
|
blocklist.appendChild(li)
|
||||||
|
})
|
||||||
|
listinstance(content, new AbortController())
|
||||||
|
modalcontent.style.animationName = 'animatetop'
|
||||||
|
modal.style.display = 'block'
|
||||||
|
var locationsearch = !new URLSearchParams(window.location.search).has('reverse') ? window.location.search ? window.location.search + '&reverse' : '?reverse' : window.location.search
|
||||||
|
history.pushState({}, null, '/' + locationsearch + '#' + content)
|
||||||
|
if (new URLSearchParams(window.location.search).has('matrix')) {
|
||||||
|
var walker = document.createTreeWalker(list, NodeFilter.SHOW_TEXT)
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
if (walker.currentNode.textContent.length > 1) {
|
||||||
|
new Messenger(walker.currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('loader-content').style.display = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
document.body.addEventListener('click', function (event) {
|
||||||
|
if (event.target == modal) {
|
||||||
|
modalcontent.style.animationName = 'animatebottom'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
closemodal.addEventListener('click', function (event) {
|
||||||
|
modalcontent.style.animationName = 'animatebottom'
|
||||||
|
})
|
||||||
|
modalcontent.addEventListener('animationend', function (event) {
|
||||||
|
if (event.animationName === 'animatebottom') {
|
||||||
|
modal.style.display = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
document.getElementById('title').addEventListener('click', function (event) {
|
||||||
|
document.getElementById('instance').value = ''
|
||||||
|
if (new URLSearchParams(window.location.search).has('matrix')) {
|
||||||
|
window.location.href = '/?matrix'
|
||||||
|
} else {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
})
|
||||||
|
document.getElementById('capture').addEventListener('click', function (event) {
|
||||||
|
html2canvas(document.querySelector('.modal-body')).then(function (canvas) {
|
||||||
|
var a = document.createElement('a')
|
||||||
|
a.download = 'fediblock-' + Date.now() + '.png'
|
||||||
|
a.href = canvas.toDataURL({ type: 'image/png' })
|
||||||
|
a.type = 'image/png'
|
||||||
|
a.target = '_blank'
|
||||||
|
a.dispatchEvent(new MouseEvent('click'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
function keypress(event, content) {
|
||||||
|
loading()
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
if (ac && ac.signal) {
|
||||||
|
ac.abort()
|
||||||
|
}
|
||||||
|
ac = new AbortController()
|
||||||
|
if (content.length === 0) {
|
||||||
|
hold = true
|
||||||
|
ranking()
|
||||||
|
} else if (content.length > 0 && !hold) {
|
||||||
|
hold = true
|
||||||
|
listinstance(content, ac)
|
||||||
|
} else if (content.length > 0 && hold) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
listinstance(content, ac)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function listinstance(content, ac) {
|
||||||
|
loading()
|
||||||
|
var list = document.getElementById('instancelist')
|
||||||
|
fetch('/api/list/' + content, { signal: ac.signal }).then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) {
|
||||||
|
while (list.hasChildNodes()) {
|
||||||
|
list.removeChild(list.firstChild)
|
||||||
|
}
|
||||||
|
res.instances.map(r => {
|
||||||
|
var li = document.createElement('li')
|
||||||
|
li.innerHTML = r.domain + (r.blocks ? ' - ' + r.blocks + ' blocks' : '')
|
||||||
|
+ (r.nodeinfo ? ' <a href="/api/detail_nodeinfo/' + r.domain + '" title="Nodeinfo for ' + r.domain + '" target="_blank">ⓘ</a>' : '')
|
||||||
|
+ (r.api.title ? '<br /> <br />' + r.api.title + ' - ' + r.api.uri + '<br />'
|
||||||
|
+ (r.last ? 'Last update: ' + (new Date(r.last)).toLocaleString() + '<br />' : '')
|
||||||
|
+ (r.api.email ? 'Email: ' + r.api.email + '<br />' : '')
|
||||||
|
+ 'Registration: ' + (r.api.registrations ? 'open' : 'closed') + ' - Version: ' + r.api.version + '<br />'
|
||||||
|
+ (r.api.stats ? 'Users: ' + r.api.stats.user_count + ' - Statuses: ' + r.api.stats.status_count + ' - Domains: ' + r.api.stats.domain_count + '<br />' : '')
|
||||||
|
+ (r.api.description ? 'Description: ' + r.api.description + '<br />' : '')
|
||||||
|
+ (r.api.thumbnail ? '<img domain="' + r.domain + '" loading="lazy" src="' + r.api.thumbnail + '" /><br />' : '') : '')
|
||||||
|
li.setAttribute('data-domain', r.domain)
|
||||||
|
li.addEventListener('click', function (event) {
|
||||||
|
if (event.target.matches('a')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
loading()
|
||||||
|
var blocklist = document.getElementById('blocklist'),
|
||||||
|
download = document.getElementById('download'),
|
||||||
|
domain = event.target.getAttribute('data-domain')
|
||||||
|
while (blocklist.hasChildNodes()) {
|
||||||
|
blocklist.removeChild(blocklist.firstChild)
|
||||||
|
}
|
||||||
|
fetch('/api/detail/' + domain).then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (res.blocks && Array.isArray(res.blocks) && res.blocks.length > 0) {
|
||||||
|
var csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n'
|
||||||
|
res.blocks.map((r, i) => {
|
||||||
|
var liblock = document.createElement('li'),
|
||||||
|
text = document.createTextNode((i + 1) + '. '),
|
||||||
|
link = '<a href="/' + (new URLSearchParams(window.location.search).has('matrix') ? '?matrix' : '') + '#' + r.domain + '" onclick="window.location.href=this.href; window.location.reload(false);">' + r.domain + '</a>',
|
||||||
|
textSeverity = document.createTextNode(r.severity ? ' - ' + r.severity : ''),
|
||||||
|
textComment = document.createTextNode(r.comment ? ' - ' + r.comment : '')
|
||||||
|
liblock.appendChild(text)
|
||||||
|
liblock.insertAdjacentHTML('beforeend', link)
|
||||||
|
liblock.appendChild(textSeverity)
|
||||||
|
liblock.appendChild(textComment)
|
||||||
|
blocklist.appendChild(liblock)
|
||||||
|
csv += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : ''
|
||||||
|
})
|
||||||
|
modalcontent.style.animationName = 'animatetop'
|
||||||
|
modal.style.display = 'block'
|
||||||
|
document.getElementById('blockcount').innerText = res.blocks.length
|
||||||
|
document.getElementById('blockinstance').innerHTML = '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/>Last update: ' + (new Date(res.last)).toLocaleString()
|
||||||
|
document.getElementById('blocktook').innerText = res.took
|
||||||
|
if (csv.split('\n').length > 2) {
|
||||||
|
download.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
|
||||||
|
download.download = 'fediblock-' + res.instance + '.csv'
|
||||||
|
download.style.display = 'inherit'
|
||||||
|
} else {
|
||||||
|
download.removeAttribute('href')
|
||||||
|
download.removeAttribute('download')
|
||||||
|
download.style.display = 'none'
|
||||||
|
}
|
||||||
|
if (new URLSearchParams(window.location.search).has('matrix')) {
|
||||||
|
var walker = document.createTreeWalker(blocklist, 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.getElementById('loader-content').style.display = 'none'
|
||||||
|
})
|
||||||
|
window.location.hash = domain
|
||||||
|
document.getElementById('loader-content').style.display = 'none'
|
||||||
|
})
|
||||||
|
list.appendChild(li)
|
||||||
|
})
|
||||||
|
var placeholder = document.getElementById('placeholder')
|
||||||
|
if (res.suggests.length > 0) {
|
||||||
|
placeholder.innerHTML = res.suggests.map(instance => '<a href="/' + (new URLSearchParams(window.location.search).has('matrix') ? '?matrix' : '') + '#' + instance + '" onclick="suggest(this.innerText);">' + instance + '</a>').join(', ')
|
||||||
|
} else {
|
||||||
|
placeholder.innerText = ''
|
||||||
|
}
|
||||||
|
if (external) {
|
||||||
|
document.getElementById('instancelist').childNodes.forEach(function (node) {
|
||||||
|
if (node.textContent.split(' ')[0].trim() === content) {
|
||||||
|
node.dispatchEvent(new MouseEvent('click'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
external = false
|
||||||
|
}
|
||||||
|
if (new URLSearchParams(window.location.search).has('matrix')) {
|
||||||
|
var walker = document.createTreeWalker(list, NodeFilter.SHOW_TEXT)
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
if (walker.currentNode.textContent.length > 1) {
|
||||||
|
new Messenger(walker.currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('loader-content').style.display = 'none'
|
||||||
|
hold = false
|
||||||
|
}).catch(err => {
|
||||||
|
// console.error(err)
|
||||||
|
document.getElementById('loader-content').style.display = 'none'
|
||||||
|
hold = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function ranking() {
|
||||||
|
loading()
|
||||||
|
var list = document.getElementById('instancelist'),
|
||||||
|
placeholder = document.getElementById('placeholder')
|
||||||
|
fetch('/api/ranking').then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (Array.isArray(res) && res.length > 0) {
|
||||||
|
while (list.hasChildNodes()) {
|
||||||
|
list.removeChild(list.firstChild)
|
||||||
|
}
|
||||||
|
var li = document.createElement('li'),
|
||||||
|
strong = document.createElement('strong'),
|
||||||
|
text = document.createTextNode('Top 100')
|
||||||
|
strong.appendChild(text)
|
||||||
|
li.appendChild(strong)
|
||||||
|
list.appendChild(li)
|
||||||
|
res.map((r, i) => {
|
||||||
|
var li = document.createElement('li'),
|
||||||
|
text = document.createTextNode(`${i + 1} - ${r.domain} - ${r.count} blocks`)
|
||||||
|
li.addEventListener('click', function (event) {
|
||||||
|
var instance = document.getElementById('instance'),
|
||||||
|
text = event.target.innerText.split(' - ')[1].trim()
|
||||||
|
instance.value = text
|
||||||
|
document.getElementById('reverse').click()
|
||||||
|
})
|
||||||
|
li.appendChild(text)
|
||||||
|
list.appendChild(li)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (new URLSearchParams(window.location.search).has('matrix')) {
|
||||||
|
var walker = document.createTreeWalker(list, NodeFilter.SHOW_TEXT)
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
if (walker.currentNode.textContent.length > 1) {
|
||||||
|
new Messenger(walker.currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
placeholder.innerText = ''
|
||||||
|
hold = false
|
||||||
|
document.getElementById('loader-content').style.display = 'none'
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
hold = false
|
||||||
|
document.getElementById('loader-content').style.display = 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function suggest(urlitem) {
|
||||||
|
var instance = document.getElementById('instance')
|
||||||
|
window.location.hash = urlitem
|
||||||
|
instance.value = urlitem
|
||||||
|
listinstance(urlitem, new AbortController())
|
||||||
|
}
|
||||||
|
function loading() {
|
||||||
|
var loader = document.getElementById('load'),
|
||||||
|
loaders = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario']
|
||||||
|
if (loader.classList.value) {
|
||||||
|
loader.classList.remove(loader.classList.value)
|
||||||
|
}
|
||||||
|
loader.classList.add(loaders[Math.floor(Math.random() * loaders.length)])
|
||||||
|
document.getElementById('loader-content').style.display = 'initial'
|
||||||
|
}
|
||||||
|
window.suggest = suggest
|
||||||
|
fetch('/api/count').then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (res && res.count) {
|
||||||
|
fetch('/api/stats').then(async function (statsresult) {
|
||||||
|
var statsres = await statsresult.json()
|
||||||
|
if (statsres) {
|
||||||
|
document.getElementById('count').innerHTML = res.count + '<div id="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>'
|
||||||
|
} else {
|
||||||
|
document.getElementById('count').innerText = res.count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetch('/api/outbox').then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (res && res.length > 0) {
|
||||||
|
var bounce = document.getElementById('bounce'),
|
||||||
|
host = new URL(window.location.href).host
|
||||||
|
reg = '(<\/?p>|(https?:\/\/)?' + host + ')'
|
||||||
|
bounce.innerHTML = res.map(p => `${p.content.replace(new RegExp(reg, 'igm'), '')} ${dayjs().to(p.published)}`).join(' | ')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetch('/api/served').then(async function (result) {
|
||||||
|
var res = await result.json()
|
||||||
|
if (res.served) {
|
||||||
|
var served = document.getElementById('served')
|
||||||
|
served.innerText = res.served
|
||||||
|
}
|
||||||
|
if (res.lastscan) {
|
||||||
|
var lastscan = document.getElementById('lastscan')
|
||||||
|
lastscan.innerText = res.lastscan
|
||||||
|
}
|
||||||
|
if (res.server) {
|
||||||
|
var server = document.getElementById('server')
|
||||||
|
server.innerHTML = '<a href="/' + (new URLSearchParams(window.location.search).has('matrix') ? '?matrix' : '') + '#' + res.server + '" onclick="window.location.href=this.href; window.location.reload(false);">' + res.server + '</a>'
|
||||||
|
}
|
||||||
|
if (res.instances) {
|
||||||
|
var instances = document.getElementById('instances')
|
||||||
|
instances.innerText = res.instances
|
||||||
|
}
|
||||||
|
if (res.peers) {
|
||||||
|
var peers = document.getElementById('peers')
|
||||||
|
peers.innerText = res.peers
|
||||||
|
}
|
||||||
|
if (res.created) {
|
||||||
|
var created = document.getElementById('created')
|
||||||
|
created.innerText = res.created
|
||||||
|
}
|
||||||
|
if (res.updated) {
|
||||||
|
var updated = document.getElementById('updated')
|
||||||
|
updated.innerText = res.updated
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (window.location.hash && window.location.hash !== null && window.location.hash !== '#') {
|
||||||
|
var instance = document.getElementById('instance'),
|
||||||
|
urlitem = window.location.hash.substring(1)
|
||||||
|
instance.value = urlitem
|
||||||
|
if (new URLSearchParams(window.location.search).has('reverse')) {
|
||||||
|
document.getElementById('reverse').click()
|
||||||
|
} else {
|
||||||
|
external = true
|
||||||
|
listinstance(urlitem, new AbortController())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ranking()
|
||||||
|
document.getElementById('instance').focus()
|
||||||
|
}
|
||||||
|
var source = new window.EventSource('/api/scan'),
|
||||||
|
scan = document.getElementById('scan')
|
||||||
|
source.onmessage = function (event) {
|
||||||
|
if (event.data) {
|
||||||
|
if (event.data.length > 0) {
|
||||||
|
scan.innerText = 'Async Scanning' + event.data
|
||||||
|
} else {
|
||||||
|
scan.innerText = 'Async Scanning...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
source.onerror = function (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
if (new URLSearchParams(window.location.search).has('matrix')) {
|
||||||
|
var walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT)
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
if (walker.currentNode.textContent.length > 1) {
|
||||||
|
new Messenger(walker.currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var matrix = document.getElementById('matrix')
|
||||||
|
matrix.href = '/'
|
||||||
|
matrix.innerText = 'matrix on'
|
||||||
|
} else {
|
||||||
|
var matrix = document.getElementById('matrix')
|
||||||
|
matrix.href = '/?matrix'
|
||||||
|
matrix.innerText = 'matrix off'
|
||||||
|
}
|
||||||
|
})
|
69
public/random-text.js
Normal file
69
public/random-text.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Messenger = Messenger;
|
1
served.txt
Normal file
1
served.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
0
|
146
server.js
Normal file
146
server.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
const apexinstance = require('./lib/apex'),
|
||||||
|
apexcustom = require('./lib/apexcustom'),
|
||||||
|
apiswagger = require('./lib/apiswagger'),
|
||||||
|
api = require('./lib/api'),
|
||||||
|
logger = require('./lib/logger'),
|
||||||
|
swagger = require('./lib/swagger'),
|
||||||
|
fediblock = require('./lib/fediblock'),
|
||||||
|
constant = require('./lib/constant'),
|
||||||
|
http = require('http'),
|
||||||
|
express = require('express'),
|
||||||
|
app = express(),
|
||||||
|
events = require('events'),
|
||||||
|
{ 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
|
||||||
|
}),
|
||||||
|
ActivitypubExpress = require('activitypub-express'),
|
||||||
|
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: 50
|
||||||
|
}),
|
||||||
|
server = http.createServer(app).listen(4000, () => {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
await apex.startDelivery()
|
||||||
|
console.log('Server listening on ' + server.address().address + ':' + server.address().port)
|
||||||
|
await fediblock(client, apex, app)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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', true)
|
||||||
|
logger(app)
|
||||||
|
app.use(
|
||||||
|
express.json({ type: apex.consts.jsonldTypes }),
|
||||||
|
express.urlencoded({ extended: true }),
|
||||||
|
apex
|
||||||
|
)
|
||||||
|
apexinstance(app, apex, routes)
|
||||||
|
apexcustom(app, apex, client)
|
||||||
|
api(app, apex, client)
|
||||||
|
apiswagger(app, client)
|
||||||
|
swagger(app)
|
||||||
|
app.use(express.static('dist'))
|
||||||
|
.use((err, req, res, next) => {
|
||||||
|
if (res.headersSent) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return next(err)
|
||||||
|
}
|
||||||
|
res.status(500).end('Error')
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user