initial commit
All checks were successful
continuous-integration/drone/tag Build is passing

This commit is contained in:
ale 2024-09-15 19:44:53 +02:00
commit e0fae7a6b4
27 changed files with 4625 additions and 0 deletions

40
.drone.yml Normal file
View 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
View File

@ -0,0 +1,5 @@
node_modules/
.parcel-cache/
logs/
dist/
yarn.lock

14
.gitlab-ci.yml Normal file
View 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
View 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
View 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
View 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:

View File

1799
fediblock-mapping.json Normal file

File diff suppressed because it is too large Load Diff

21
lib/apex.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

77
public/index.html Normal file
View 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 &#934;</a></h1>
<h4>
<p id="bounce">&nbsp;</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> &copy;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">&times;</span>
<a id="capture" title="Take snapshot">&#128247;</a>
<a id="download" title="Download fediblock CSV mastodon file">&#8681;</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
View 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
View 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
View 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">&#9432;</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 ? '&nbsp;<a href="/api/detail_nodeinfo/' + res.instance + '" title="Nodeinfo for ' + res.instance + '" target="_blank">&#9432;</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
View 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
View File

@ -0,0 +1 @@
0

146
server.js Normal file
View 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')
})