module.exports = (app, client) => { const constant = require('./constant'), zlib = require('zlib'), { pick } = require('stream-json/filters/Pick'), { parser } = require('stream-json'), { streamArray } = require('stream-json/streamers/StreamArray'), { chain } = require('stream-chain'), 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?.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?.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?.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?.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: 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) } } } } }, { asStream: true, meta: false }), pipeline = chain([ parser(), pick({ filter: 'hits.hits' }), streamArray(), data => ({ instance: data.value._source.instance, comment: data.value._source.blocks.find(block => block.domain === clean(req.params.instance)).comment }) ]) pipeline.on('readable', () => res.write('[')) pipeline.on('data', data => res.write(data.toString() + ',')) pipeline.on('end', () => res.end(']')) result.pipe(pipeline) } else { res.status(404).end() } }) /** * @swagger * /api/download_index: * get: * summary: Retrieve all content of ElasticSearch index. * description: Retrieve all content of ElasticSearch index. * responses: * 200: * description: A file compressed with gzip. * content: * application/gzip: */ app.use('/api/download_index', async (req, res) => { try { res.setHeader('Content-Type', 'application/gzip') res.setHeader('Content-disposition', 'attachment; filename=fediblock-index.jsonl.gz') const result = await client.search({ index: constant.index, size: 9999, query: { match_all: {} } }, { asStream: true, meta: false }), pipeline = chain([ parser(), pick({ filter: 'hits.hits' }), streamArray(), data => JSON.stringify(data.value._source) + '\n', zlib.createGzip() ]) result.pipe(pipeline).pipe(res) } catch (e) { console.error(e) res.status(404).end() } }) }