From bfaab359eb31c6da4679576c2b7d904c25606fdd Mon Sep 17 00:00:00 2001 From: ale Date: Fri, 18 Jul 2025 22:19:35 +0200 Subject: [PATCH] initial commit Signed-off-by: ale --- .drone.yml | 35 + .gitignore | 3 + Dockerfile | 11 + README.md | 42 + back/constant.js | 17 + back/fediblock-mapping.json | 7191 +++++++++++++++++++++++++++++++++ back/index.js | 3 + back/lib/apex.js | 33 + back/lib/apexcustom.js | 97 + back/lib/api.js | 54 + back/lib/apiswagger.js | 545 +++ back/lib/express.js | 158 + back/lib/fediblock.js | 211 + back/lib/logger.js | 6 + back/lib/swagger.js | 25 + back/lib/taskdeletedup.js | 69 + back/lib/util.js | 83 + back/package.json | 35 + back/served.txt | 1 + docker-compose.yml | 55 + front/.gitignore | 23 + front/README.md | 70 + front/package.json | 38 + front/public/favicon.ico | Bin 0 -> 31014 bytes front/public/index.html | 21 + front/public/logo192.png | Bin 0 -> 5347 bytes front/public/logo512.png | Bin 0 -> 9664 bytes front/public/manifest.json | 15 + front/public/robots.txt | 3 + front/src/App.css | 405 ++ front/src/App.js | 28 + front/src/App.test.js | 8 + front/src/component/Bar.js | 29 + front/src/component/Count.js | 41 + front/src/component/Footer.js | 67 + front/src/component/Form.js | 167 + front/src/component/Loader.js | 19 + front/src/component/Modal.js | 142 + front/src/component/Scan.js | 31 + front/src/component/Title.js | 13 + front/src/index.js | 12 + front/src/loaders.css | 411 ++ front/src/logo.svg | 1 + front/src/random-text.js | 69 + front/src/reportWebVitals.js | 13 + front/src/setupTests.js | 5 + 46 files changed, 10305 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 back/constant.js create mode 100644 back/fediblock-mapping.json create mode 100644 back/index.js create mode 100644 back/lib/apex.js create mode 100644 back/lib/apexcustom.js create mode 100644 back/lib/api.js create mode 100644 back/lib/apiswagger.js create mode 100644 back/lib/express.js create mode 100644 back/lib/fediblock.js create mode 100644 back/lib/logger.js create mode 100644 back/lib/swagger.js create mode 100644 back/lib/taskdeletedup.js create mode 100644 back/lib/util.js create mode 100644 back/package.json create mode 100644 back/served.txt create mode 100644 docker-compose.yml create mode 100644 front/.gitignore create mode 100644 front/README.md create mode 100644 front/package.json create mode 100644 front/public/favicon.ico create mode 100644 front/public/index.html create mode 100644 front/public/logo192.png create mode 100644 front/public/logo512.png create mode 100644 front/public/manifest.json create mode 100644 front/public/robots.txt create mode 100644 front/src/App.css create mode 100644 front/src/App.js create mode 100644 front/src/App.test.js create mode 100644 front/src/component/Bar.js create mode 100644 front/src/component/Count.js create mode 100644 front/src/component/Footer.js create mode 100644 front/src/component/Form.js create mode 100644 front/src/component/Loader.js create mode 100644 front/src/component/Modal.js create mode 100644 front/src/component/Scan.js create mode 100644 front/src/component/Title.js create mode 100644 front/src/index.js create mode 100644 front/src/loaders.css create mode 100644 front/src/logo.svg create mode 100644 front/src/random-text.js create mode 100644 front/src/reportWebVitals.js create mode 100644 front/src/setupTests.js diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..8a8e013 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,35 @@ +kind: pipeline +name: code-build-linux-amd64 +type: docker + +platform: + os: linux + arch: arm64 + +steps: +- name: build + image: docker:dind + privileged: true + environment: + USER: + from_secret: user + PASS: + from_secret: pass + REGISTRY: + from_secret: registry + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - docker login -u $USER -p $PASS $REGISTRY + - docker buildx build -t $REGISTRY/fediblock-instance . + - docker push $REGISTRY/fediblock-instance + when: + event: + - push + - tag + +volumes: +- name: dockersock + host: + path: /var/run/docker.sock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d893bfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/node_modules/ +**/*.lock +**/*-lock.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c066b43 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-slim +RUN apt update && apt install -y git && apt clean +COPY --chown=node:node . /fediblock-instance +USER node +RUN yarn config set network-timeout 300000 +WORKDIR /fediblock-instance/front +RUN yarn +WORKDIR /fediblock-instance/back +RUN yarn && yarn build +EXPOSE 4000 +ENTRYPOINT ["yarn", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e43b6ad --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Fediblock Instance + +## Another instance to search public blocks and do stats and more, like one FBA but with NodeJS powers. + +## We need one mongodb instance for federation and one elasticsearch node to storage data. + +## Install + +``` +$ yarn or npm i +``` + +## Build frontend + +``` +$ yarn build or npm run build +``` + +## Configuration + +- edit `back/constant.js` with your environment preferences + +## Start + +``` +$ yarn start or npm run start +``` + +## Docker + +``` +$ docker compose build or docker pull registry.manalejandro.com/fediblock-instance +$ docker compose pull +$ docker compose up -d +``` + +### Best deploy with subdomain in one HTTPS proxy like nginx or apache throught 4000/tcp port + + +## License + +MIT \ No newline at end of file diff --git a/back/constant.js b/back/constant.js new file mode 100644 index 0000000..39b2408 --- /dev/null +++ b/back/constant.js @@ -0,0 +1,17 @@ +module.exports = { + index: 'fediblock', // elasticsearch index name + nick: 'fediblockbot', // nick of bot + icon: 'https://manalejandro.com/favicon.png', // icon of the instance + dburl: 'mongodb://fediblock-mongodb:27017', // mongodb connection + dbname: 'fediblock', // mongodb database name + apexdomain: 'fediblock.manalejandro.com', // domain of the instance + agent: 'fediblock.manalejandro.com', // agent of fetch requests + elasticnode: 'http://fediblock-elasticsearch:9200', // elasticsearch connection + workers: 3, // async concurrent workers to scan + threads: 2, // threads of each worker + schedule: '5,11,17,23', // UTC hours to publish bot followers federated stats + taskdeletedup: '6', // task hour to delete dups index instances entries + filterdomains: ['activitypub-troll.cf'], // domains filtered to scan + initialscan: 'mastodon.social', // initial federated domain to scan + abort_timeout: 5000 // Timeout of abort scan request +} diff --git a/back/fediblock-mapping.json b/back/fediblock-mapping.json new file mode 100644 index 0000000..6621f16 --- /dev/null +++ b/back/fediblock-mapping.json @@ -0,0 +1,7191 @@ +{ + "mappings": { + "properties": { + "api": { + "properties": { + "accent_color": { + "type": "text" + }, + "account_domain": { + "type": "text" + }, + "allow_unauthenticated": { + "properties": { + "timeline_local": { + "type": "boolean" + } + } + }, + "altids": { + "type": "object", + "enabled": false + }, + "api_schema": { + "type": "text" + }, + "approval_required": { + "type": "boolean" + }, + "asana-client-id": { + "type": "text" + }, + "atlassian-api-key": { + "type": "text" + }, + "authentication_methods": { + "type": "text" + }, + "avatar_upload_limit": { + "type": "long" + }, + "aws-access-secret": { + "type": "text" + }, + "background_image": { + "type": "text" + }, + "background_upload_limit": { + "type": "long" + }, + "banner_upload_limit": { + "type": "long" + }, + "bitbucket-client-id": { + "type": "text" + }, + "bitbucket-client-key": { + "type": "text" + }, + "blockchains": { + "properties": { + "chain_id": { + "type": "text" + }, + "features": { + "properties": { + "gate": { + "type": "boolean" + }, + "minter": { + "type": "boolean" + }, + "subscriptions": { + "type": "boolean" + } + } + } + } + }, + "buffer": { + "type": "long" + }, + "chat_limit": { + "type": "long" + }, + "command": { + "type": "text" + }, + "compile": { + "type": "text" + }, + "config": { + "properties": { + "dropbox-service-secret": { + "type": "text" + }, + "environments": { + "properties": { + "datadog-access-key": { + "type": "text" + }, + "dev": { + "properties": { + "aws-access-key": { + "type": "text" + }, + "flickr-access-token": { + "type": "text" + }, + "mapbox-api-token": { + "type": "text" + }, + "phases": { + "properties": { + "arrivalRate": { + "type": "long" + }, + "duration": { + "type": "long" + } + } + }, + "sidekiq-key": { + "type": "text" + }, + "target": { + "type": "text" + } + } + }, + "qa": { + "properties": { + "bittrex-token-key": { + "type": "text" + }, + "confluent-access-secret": { + "type": "text" + }, + "easypost-test-api-secret": { + "type": "text" + }, + "phases": { + "properties": { + "arrivalRate": { + "type": "long" + }, + "duration": { + "type": "long" + } + } + }, + "target": { + "type": "text" + }, + "twitter-api-token": { + "type": "text" + } + } + }, + "shopify-private-app-access-token": { + "type": "text" + }, + "slack-app-secret": { + "type": "text" + }, + "travisci-access-token": { + "type": "text" + } + } + }, + "frameio-service-key": { + "type": "text" + }, + "http": { + "properties": { + "freshbooks-access-key": { + "type": "text" + }, + "kucoin-secret-token": { + "type": "text" + }, + "linear-client-token": { + "type": "text" + }, + "npm-access-token": { + "type": "text" + }, + "timeout": { + "type": "long" + } + } + }, + "phases": { + "properties": { + "arrivalRate": { + "type": "long" + }, + "duration": { + "type": "long" + }, + "name": { + "type": "text" + } + } + }, + "plugins": { + "properties": { + "discord-client-secret": { + "type": "text" + }, + "expect": { + "properties": { + "bitbucket-client-id": { + "type": "text" + }, + "hashicorp-tf-api-secret": { + "type": "text" + }, + "outputFormat": { + "type": "text" + }, + "shopify-private-app-access-key": { + "type": "text" + }, + "snyk-service-secret": { + "type": "text" + } + } + }, + "metrics-by-endpoint": { + "properties": { + "doppler-service-secret": { + "type": "text" + }, + "hubspot-service-token": { + "type": "text" + }, + "linear-client-key": { + "type": "text" + }, + "shopify-access-key": { + "type": "text" + }, + "useOnlyRequestNames": { + "type": "boolean" + } + } + }, + "openai-service-token": { + "type": "text" + }, + "publish-metrics": { + "properties": { + "event": { + "properties": { + "priority": { + "type": "text" + }, + "send": { + "type": "boolean" + }, + "status": { + "type": "text" + }, + "tags": { + "type": "text" + }, + "title": { + "type": "text" + } + } + }, + "host": { + "type": "text" + }, + "port": { + "type": "text" + }, + "prefix": { + "type": "text" + }, + "tags": { + "type": "text" + }, + "type": { + "type": "text" + } + } + }, + "slack-config-refresh-secret": { + "type": "text" + }, + "sumologic-access-key": { + "type": "text" + } + } + }, + "processor": { + "type": "text" + }, + "sentry-access-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "tls": { + "properties": { + "adafruit-service-key": { + "type": "text" + }, + "dropbox-short-lived-service-secret": { + "type": "text" + }, + "openai-api-secret": { + "type": "text" + }, + "rejectUnauthorized": { + "type": "boolean" + }, + "twitter-api-secret": { + "type": "text" + } + } + }, + "twilio-api-token": { + "type": "text" + } + } + }, + "configuration": { + "properties": { + "accounts": { + "properties": { + "allow_custom_css": { + "type": "boolean" + }, + "characters_reserved_per_emoji": { + "type": "long" + }, + "max_display_name": { + "type": "long" + }, + "max_favourite_tags": { + "type": "long" + }, + "max_featured_tags": { + "type": "long" + }, + "max_profile_fields": { + "type": "long" + }, + "max_status_pins": { + "type": "long" + } + } + }, + "contact_account": { + "type": "text" + }, + "emoji_reactions": { + "properties": { + "max_reactions": { + "type": "long" + }, + "max_reactions_per_account": { + "type": "long" + }, + "max_reactions_per_remote_account": { + "type": "long" + } + } + }, + "emojis": { + "properties": { + "emoji_size_limit": { + "type": "long" + } + } + }, + "media_attachment": { + "properties": { + "image_size_limit": { + "type": "long" + }, + "supported_mime_types": { + "type": "text" + }, + "video_size_limit": { + "type": "long" + } + } + }, + "media_attachments": { + "properties": { + "attachments_limit": { + "type": "long" + }, + "image_matrix_limit": { + "type": "long" + }, + "image_size_limit": { + "type": "long" + }, + "max_characters": { + "type": "long" + }, + "polls": { + "properties": { + "max_characters_per_option": { + "type": "long" + }, + "max_expiration": { + "type": "long" + }, + "max_options": { + "type": "long" + }, + "min_expiration": { + "type": "long" + } + } + }, + "supported_mime_types": { + "type": "text" + }, + "video_frame_limit": { + "type": "long" + }, + "video_frame_rate_limit": { + "type": "long" + }, + "video_matrix_limit": { + "type": "long" + }, + "video_size_limit": { + "type": "long" + } + } + }, + "oidc_enabled": { + "type": "boolean" + }, + "polls": { + "properties": { + "allow_image": { + "type": "boolean" + }, + "allow_media": { + "type": "boolean" + }, + "max_characters_per_option": { + "type": "long" + }, + "max_expiration": { + "type": "double" + }, + "max_options": { + "type": "long" + }, + "min_expiration": { + "type": "double" + }, + "min_options": { + "type": "long" + } + } + }, + "reaction_deck": { + "properties": { + "max_emojis": { + "type": "long" + } + } + }, + "reactions": { + "properties": { + "default_reaction": { + "type": "text" + }, + "max_reactions": { + "type": "long" + }, + "max_reactions_per_account": { + "type": "long" + } + } + }, + "rules": { + "properties": { + "id": { + "type": "long" + }, + "text": { + "type": "text" + } + } + }, + "search": { + "properties": { + "enabled": { + "type": "boolean" + }, + "supported_operator": { + "type": "text" + }, + "supported_order": { + "type": "text" + }, + "supported_prefix": { + "type": "text" + }, + "supported_properties": { + "type": "text" + }, + "supported_searchablity_filter": { + "type": "text" + } + } + }, + "status_references": { + "properties": { + "max_references": { + "type": "long" + } + } + }, + "statuses": { + "properties": { + "characters_reserved_per_url": { + "type": "long" + }, + "cw_max_characters": { + "type": "long" + }, + "max_characters": { + "type": "long" + }, + "max_expiration": { + "type": "double" + }, + "max_media_attachments": { + "type": "long" + }, + "max_media_attachments_from_activitypub": { + "type": "long" + }, + "max_media_attachments_with_poll": { + "type": "long" + }, + "min_expiration": { + "type": "double" + }, + "supported_expires_actions": { + "type": "text" + }, + "supported_mime_types": { + "type": "text" + }, + "supported_toggles": { + "properties": { + "local_only": { + "type": "boolean" + }, + "sensitive": { + "type": "boolean" + } + } + }, + "supported_visibility": { + "properties": { + "direct": { + "type": "boolean" + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "unlisted": { + "type": "boolean" + } + } + } + } + }, + "translation": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "urls": { + "type": "object", + "enabled": false + } + } + }, + "contact_account": { + "properties": { + "acct": { + "type": "keyword" + }, + "actor_id": { + "type": "text" + }, + "all_emojis": { + "properties": { + "shortcode": { + "type": "text" + }, + "static_url": { + "type": "text" + }, + "url": { + "type": "text" + }, + "visible_in_picker": { + "type": "boolean" + } + } + }, + "avatar": { + "type": "text" + }, + "avatar_description": { + "type": "text" + }, + "avatar_frame_type": { + "type": "text" + }, + "avatar_media_id": { + "type": "text" + }, + "avatar_static": { + "type": "text" + }, + "avatar_thumbhash": { + "type": "text" + }, + "bot": { + "type": "boolean" + }, + "cat": { + "type": "boolean" + }, + "created_at": { + "type": "date" + }, + "custom_css": { + "type": "text" + }, + "discoverable": { + "type": "boolean" + }, + "display_name": { + "type": "text" + }, + "emoji_reaction_available_server": { + "type": "boolean" + }, + "emojis": { + "properties": { + "aliases": { + "type": "text" + }, + "category": { + "type": "text" + }, + "height": { + "type": "long" + }, + "is_sensitive": { + "type": "boolean" + }, + "sensitive": { + "type": "boolean" + }, + "shortcode": { + "type": "text" + }, + "static_url": { + "type": "text" + }, + "thumbhash": { + "type": "text" + }, + "url": { + "type": "text" + }, + "visible_in_picker": { + "type": "boolean" + }, + "width": { + "type": "long" + } + } + }, + "enable_rss": { + "type": "boolean" + }, + "fields": { + "properties": { + "is_legacy_proof": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "value": { + "type": "text" + }, + "verified_at": { + "type": "date" + } + } + }, + "followers_count": { + "type": "long" + }, + "following_count": { + "type": "long" + }, + "fqn": { + "type": "text" + }, + "group": { + "type": "boolean" + }, + "header": { + "type": "text" + }, + "header_description": { + "type": "text" + }, + "header_media_id": { + "type": "text" + }, + "header_static": { + "type": "text" + }, + "header_thumbhash": { + "type": "text" + }, + "hide_boosts": { + "type": "boolean" + }, + "hide_collections": { + "type": "boolean" + }, + "id": { + "type": "text" + }, + "identity_proofs": { + "properties": { + "is_legacy_proof": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "value": { + "type": "text" + }, + "verified_at": { + "type": "date" + } + } + }, + "indexable": { + "type": "boolean" + }, + "is_admin": { + "type": "boolean" + }, + "jam_identity": { + "type": "text" + }, + "last_status_at": { + "type": "date" + }, + "liker_id": { + "type": "text" + }, + "limited": { + "type": "boolean" + }, + "local": { + "type": "boolean" + }, + "locked": { + "type": "boolean" + }, + "memorial": { + "type": "boolean" + }, + "mention_policy": { + "type": "text" + }, + "moved": { + "properties": { + "acct": { + "type": "text" + }, + "avatar": { + "type": "text" + }, + "avatar_static": { + "type": "text" + }, + "bot": { + "type": "boolean" + }, + "created_at": { + "type": "date" + }, + "discoverable": { + "type": "boolean" + }, + "display_name": { + "type": "text" + }, + "emojis": { + "properties": { + "shortcode": { + "type": "text" + }, + "static_url": { + "type": "text" + }, + "url": { + "type": "text" + }, + "visible_in_picker": { + "type": "boolean" + } + } + }, + "fields": { + "properties": { + "name": { + "type": "text" + }, + "value": { + "type": "text" + }, + "verified_at": { + "type": "date" + } + } + }, + "followers_count": { + "type": "long" + }, + "following_count": { + "type": "long" + }, + "group": { + "type": "boolean" + }, + "header": { + "type": "text" + }, + "header_static": { + "type": "text" + }, + "hide_collections": { + "type": "boolean" + }, + "id": { + "type": "text" + }, + "indexable": { + "type": "boolean" + }, + "last_status_at": { + "type": "date" + }, + "locked": { + "type": "boolean" + }, + "noindex": { + "type": "boolean" + }, + "note": { + "type": "text" + }, + "roles": { + "properties": { + "color": { + "type": "text" + }, + "id": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "statuses_count": { + "type": "long" + }, + "uri": { + "type": "text" + }, + "url": { + "type": "text" + }, + "username": { + "type": "text" + } + } + }, + "noindex": { + "type": "boolean" + }, + "note": { + "type": "text" + }, + "note_text": { + "type": "text" + }, + "other_settings": { + "properties": { + "allow_quote": { + "type": "boolean" + }, + "birth_day": { + "type": "long" + }, + "birth_month": { + "type": "long" + }, + "cat_ears_color": { + "type": "text" + }, + "emoji_reaction_policy": { + "type": "text" + }, + "enable_reaction": { + "type": "boolean" + }, + "hide_followers_count": { + "type": "boolean" + }, + "hide_following_count": { + "type": "boolean" + }, + "hide_network": { + "type": "boolean" + }, + "hide_statuses_count": { + "type": "boolean" + }, + "location": { + "type": "text" + }, + "noai": { + "type": "boolean" + }, + "noindex": { + "type": "boolean" + }, + "subscription_policy": { + "type": "text" + }, + "translatable_private": { + "type": "boolean" + } + } + }, + "payment_options": { + "properties": { + "chain_id": { + "type": "text" + }, + "price": { + "type": "long" + }, + "type": { + "type": "text" + } + } + }, + "pleroma": { + "properties": { + "accepts_chat_messages": { + "type": "boolean" + }, + "also_known_as": { + "type": "text" + }, + "ap_id": { + "type": "text" + }, + "avatar_description": { + "type": "text" + }, + "background_image": { + "type": "text" + }, + "birthday": { + "type": "date" + }, + "favicon": { + "type": "text" + }, + "header_description": { + "type": "text" + }, + "hide_favorites": { + "type": "boolean" + }, + "hide_followers": { + "type": "boolean" + }, + "hide_followers_count": { + "type": "boolean" + }, + "hide_follows": { + "type": "boolean" + }, + "hide_follows_count": { + "type": "boolean" + }, + "is_admin": { + "type": "boolean" + }, + "is_confirmed": { + "type": "boolean" + }, + "is_local": { + "type": "boolean" + }, + "is_moderator": { + "type": "boolean" + }, + "is_suggested": { + "type": "boolean" + }, + "location": { + "type": "text" + }, + "permit_followback": { + "type": "boolean" + }, + "privileges": { + "type": "text" + }, + "relationship": { + "type": "object" + }, + "skip_thread_containment": { + "type": "boolean" + }, + "tags": { + "type": "text" + } + } + }, + "pronouns": { + "type": "text" + }, + "role": { + "properties": { + "name": { + "type": "text" + } + } + }, + "roles": { + "properties": { + "color": { + "type": "text" + }, + "id": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "searchability": { + "type": "text" + }, + "server_features": { + "properties": { + "circle": { + "type": "boolean" + }, + "emoji_reaction": { + "type": "boolean" + }, + "quote": { + "type": "boolean" + }, + "status_reference": { + "type": "boolean" + } + } + }, + "source": { + "properties": { + "fields": { + "properties": { + "name": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "follow_requests_count": { + "type": "long" + }, + "language": { + "type": "text" + }, + "note": { + "type": "text" + }, + "pleroma": { + "properties": { + "actor_type": { + "type": "text" + }, + "discoverable": { + "type": "boolean" + } + } + }, + "privacy": { + "type": "text" + }, + "sensitive": { + "type": "boolean" + } + } + }, + "statuses_count": { + "type": "long" + }, + "subscribable": { + "type": "boolean" + }, + "subscribers_count": { + "type": "long" + }, + "subscribing_count": { + "type": "long" + }, + "suspended": { + "type": "boolean" + }, + "theme": { + "type": "text" + }, + "uri": { + "type": "text" + }, + "url": { + "type": "text" + }, + "username": { + "type": "text" + }, + "website": { + "type": "text" + } + } + }, + "content": { + "type": "text" + }, + "custom_css": { + "type": "text" + }, + "default_approval": { + "type": "boolean" + }, + "description": { + "type": "text" + }, + "description_limit": { + "type": "long" + }, + "description_source": { + "type": "text" + }, + "description_text": { + "type": "text" + }, + "digitalocean-access-key": { + "type": "text" + }, + "discord-client-id": { + "type": "text" + }, + "discord-client-token": { + "type": "text" + }, + "domain": { + "type": "text" + }, + "email": { + "type": "text" + }, + "embargoed": { + "type": "date" + }, + "errors": { + "properties": { + "detail": { + "type": "text" + } + } + }, + "event": { + "properties": { + "code": { + "type": "text" + }, + "name": { + "type": "text" + }, + "rel": { + "type": "text" + }, + "scheme": { + "type": "text" + } + } + }, + "exec": { + "type": "text" + }, + "feature_quote": { + "type": "boolean" + }, + "federated_timeline_restricted": { + "type": "boolean" + }, + "fedibird_capabilities": { + "type": "text" + }, + "finicity-client-key": { + "type": "text" + }, + "finnhub-access-token": { + "type": "text" + }, + "frameworks": { + "properties": { + "additional_0": { + "properties": { + "beamer-api-secret": { + "type": "text" + }, + "compilationOptions": { + "properties": { + "allowUnsafe": { + "type": "boolean" + }, + "flickr-access-token": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "optimize": { + "type": "boolean" + }, + "postman-api-key": { + "type": "text" + }, + "slack-config-access-secret": { + "type": "text" + }, + "warningsAsErrors": { + "type": "boolean" + } + } + }, + "dependencies": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "properties": { + "planetscale-service-secret": { + "type": "text" + }, + "private-key": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + }, + "sumologic-access-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "github-oauth": { + "type": "text" + }, + "linkedin-client-key": { + "type": "text" + }, + "shopify-shared-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + }, + "zendesk-token-secret": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "alibaba-key-secret": { + "type": "text" + }, + "jfrog-identity-token": { + "type": "text" + }, + "mailgun-pub-token": { + "type": "text" + }, + "new-relic-browser-api-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "digitalocean-refresh-key": { + "type": "text" + }, + "flickr-access-secret": { + "type": "text" + }, + "gitlab-pat": { + "type": "text" + }, + "telegram-bot-api-secret": { + "type": "text" + } + } + }, + "additional_5": { + "type": "text" + }, + "digitalocean-access-token": { + "type": "text" + }, + "flutterwave-encryption-secret": { + "type": "text" + }, + "kucoin-secret-key": { + "type": "text" + }, + "new-relic-browser-service-key": { + "type": "text" + } + } + }, + "frameworkAssemblies": { + "properties": { + "additional_0": { + "properties": { + "alibaba-access-token-id": { + "type": "text" + }, + "defined-networking-api-secret": { + "type": "text" + }, + "discord-service-token": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "flutterwave-public-token": { + "type": "text" + }, + "frameio-service-token": { + "type": "text" + }, + "launchdarkly-access-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twitter-access-secret": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "adafruit-service-key": { + "type": "text" + }, + "defined-networking-api-key": { + "type": "text" + }, + "grafana-cloud-service-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + }, + "zendesk-token-secret": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "plaid-key-key": { + "type": "text" + }, + "planetscale-oauth-secret": { + "type": "text" + }, + "readme-service-key": { + "type": "text" + }, + "slack-user-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "github-fine-grained-pat": { + "type": "text" + }, + "sendgrid-service-secret": { + "type": "text" + }, + "shopify-private-app-access-key": { + "type": "text" + }, + "stripe-access-token": { + "type": "text" + } + } + }, + "gitlab-pat": { + "type": "text" + }, + "linear-client-token": { + "type": "text" + }, + "planetscale-oauth-token": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "dependencies": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "properties": { + "github-fine-grained-pat": { + "type": "text" + }, + "plaid-api-secret": { + "type": "text" + }, + "plaid-service-token": { + "type": "text" + }, + "sumologic-access-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "atlassian-service-secret": { + "type": "text" + }, + "bitbucket-client-id": { + "type": "text" + }, + "duffel-service-key": { + "type": "text" + }, + "square-access-secret": { + "type": "text" + } + } + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "properties": { + "defined-networking-api-secret": { + "type": "text" + }, + "finnhub-access-secret": { + "type": "text" + }, + "grafana-api-account-token": { + "type": "text" + }, + "hubspot-api-secret": { + "type": "text" + }, + "target": { + "type": "text" + } + } + }, + "microsoft-teams-webhook": { + "type": "text" + }, + "plaid-secret-key": { + "type": "text" + }, + "squarespace-access-token": { + "type": "text" + }, + "telegram-bot-api-secret": { + "type": "text" + } + } + }, + "finicity-client-key": { + "type": "text" + }, + "finicity-client-secret": { + "type": "text" + }, + "finnhub-access-token": { + "type": "text" + }, + "rservicedapi-access-token": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "compilationOptions": { + "properties": { + "flickr-access-secret": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "grafana-service-account-key": { + "type": "text" + }, + "private-secret": { + "type": "text" + } + } + }, + "dependencies": { + "properties": { + "additional_0": { + "type": "text" + }, + "asana-client-id": { + "type": "text" + }, + "bittrex-access-token": { + "type": "text" + }, + "grafana-api-account-key": { + "type": "text" + }, + "twitch-api-key": { + "type": "text" + } + } + }, + "frameworkAssemblies": { + "properties": { + "additional_0": { + "properties": { + "github-oauth": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + }, + "square-access-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twilio-service-key": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "contentful-delivery-service-secret": { + "type": "text" + }, + "freshbooks-access-secret": { + "type": "text" + }, + "linear-api-token": { + "type": "text" + }, + "sumologic-access-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "authress-api-client-access-token": { + "type": "text" + }, + "gitlab-rrt": { + "type": "text" + }, + "linear-api-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twitter-bearer-token": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "properties": { + "asana-client-id": { + "type": "text" + }, + "dropbox-short-lived-api-token": { + "type": "text" + }, + "sendinblue-api-token": { + "type": "text" + }, + "snyk-service-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_7": { + "properties": { + "adobe-client-token": { + "type": "text" + }, + "authress-service-client-access-secret": { + "type": "text" + }, + "finicity-api-token": { + "type": "text" + }, + "openai-service-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "doppler-api-key": { + "type": "text" + }, + "easypost-api-token": { + "type": "text" + }, + "mailchimp-service-key": { + "type": "text" + }, + "shopify-custom-access-token": { + "type": "text" + } + } + }, + "gitlab-pat": { + "type": "text" + }, + "sendbird-access-key": { + "type": "text" + }, + "sidekiq-token": { + "type": "text" + }, + "telegram-bot-service-token": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "adafruit-api-secret": { + "type": "text" + }, + "compilationOptions": { + "properties": { + "allowUnsafe": { + "type": "boolean" + }, + "define": { + "type": "text" + }, + "delaySign": { + "type": "boolean" + }, + "discord-client-key": { + "type": "text" + }, + "emitEntryPoint": { + "type": "boolean" + }, + "freshbooks-access-secret": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "keyFile": { + "type": "text" + }, + "languageVersion": { + "type": "text" + }, + "linkedin-client-secret": { + "type": "text" + }, + "optimize": { + "type": "boolean" + }, + "useOssSigning": { + "type": "boolean" + }, + "warningsAsErrors": { + "type": "boolean" + } + } + }, + "dependencies": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "properties": { + "asana-client-token": { + "type": "text" + }, + "jwt": { + "type": "text" + }, + "plaid-api-key": { + "type": "text" + }, + "prefect-api-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "properties": { + "flutterwave-encryption-token": { + "type": "text" + }, + "github-pat": { + "type": "text" + }, + "pulumi-service-secret": { + "type": "text" + }, + "sumologic-access-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "type": "text" + }, + "additional_7": { + "type": "text" + }, + "defined-networking-service-key": { + "type": "text" + }, + "facebook": { + "type": "text" + }, + "twitter-api-token": { + "type": "text" + }, + "vault-api-token": { + "type": "text" + } + } + }, + "frameworkAssemblies": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "properties": { + "discord-client-secret": { + "type": "text" + }, + "easypost-service-key": { + "type": "text" + }, + "gcp-api-key": { + "type": "text" + }, + "planetscale-oauth-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "duffel-service-key": { + "type": "text" + }, + "kucoin-access-key": { + "type": "text" + }, + "rapidapi-access-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "travisci-access-key": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "dynatrace-api-token": { + "type": "text" + }, + "lob-service-token": { + "type": "text" + }, + "shopify-custom-access-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twitter-bearer-token": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_5": { + "type": "text" + }, + "facebook": { + "type": "text" + }, + "jfrog-service-secret": { + "type": "text" + }, + "launchdarkly-access-key": { + "type": "text" + }, + "npm-access-key": { + "type": "text" + } + } + }, + "mailchimp-service-secret": { + "type": "text" + }, + "shopify-access-token": { + "type": "text" + }, + "twitter-service-token": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "beamer-api-secret": { + "type": "text" + }, + "netlify-access-token": { + "type": "text" + }, + "slack-legacy-bot-secret": { + "type": "text" + }, + "snyk-api-key": { + "type": "text" + } + } + }, + "additional_5": { + "properties": { + "dependencies": { + "properties": { + "additional_0": { + "properties": { + "discord-api-key": { + "type": "text" + }, + "lob-pub-api-token": { + "type": "text" + }, + "messagebird-client-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twitter-service-secret": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "alibaba-access-secret-id": { + "type": "text" + }, + "datadog-access-token": { + "type": "text" + }, + "new-relic-user-api-token": { + "type": "text" + }, + "pypi-upload-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "age key key": { + "type": "text" + }, + "intercom-service-key": { + "type": "text" + }, + "stripe-access-key": { + "type": "text" + }, + "sumologic-access-id": { + "type": "text" + } + } + }, + "frameworkAssemblies": { + "properties": { + "additional_0": { + "properties": { + "adafruit-api-key": { + "type": "text" + }, + "authress-api-client-access-token": { + "type": "text" + }, + "rubygems-service-token": { + "type": "text" + }, + "shopify-access-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "properties": { + "algolia-api-key": { + "type": "text" + }, + "databricks-api-key": { + "type": "text" + }, + "gitlab-pat": { + "type": "text" + }, + "sumologic-access-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "adobe-client-key": { + "type": "text" + }, + "frameio-api-secret": { + "type": "text" + }, + "shopify-custom-access-token": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twitch-api-token": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "codecov-access-token": { + "type": "text" + }, + "droneci-access-secret": { + "type": "text" + }, + "frameio-api-token": { + "type": "text" + }, + "sendgrid-api-key": { + "type": "text" + } + } + }, + "freshbooks-access-key": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "plaid-api-secret": { + "type": "text" + }, + "slack-legacy-workspace-key": { + "type": "text" + } + } + }, + "additional_6": { + "properties": { + "confluent-access-secret": { + "type": "text" + }, + "finnhub-access-secret": { + "type": "text" + }, + "pulumi-api-token": { + "type": "text" + }, + "readme-service-token": { + "type": "text" + } + } + }, + "additional_7": { + "properties": { + "compilationOptions": { + "properties": { + "confluent-access-secret": { + "type": "text" + }, + "dropbox-short-lived-service-secret": { + "type": "text" + }, + "keyFile": { + "type": "text" + }, + "languageVersion": { + "type": "text" + }, + "mailgun-private-api-key": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + } + } + }, + "dependencies": { + "properties": { + "additional_0": { + "properties": { + "contentful-delivery-api-token": { + "type": "text" + }, + "jwt": { + "type": "text" + }, + "planetscale-oauth-key": { + "type": "text" + }, + "slack-legacy-bot-secret": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "droneci-access-token": { + "type": "text" + }, + "twilio-api-token": { + "type": "text" + }, + "vault-batch-key": { + "type": "text" + }, + "zendesk-key-key": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "codecov-access-key": { + "type": "text" + }, + "databricks-service-key": { + "type": "text" + }, + "mailgun-pub-secret": { + "type": "text" + }, + "sendbird-access-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "age key key": { + "type": "text" + }, + "droneci-access-token": { + "type": "text" + }, + "slack-user-secret": { + "type": "text" + }, + "travisci-access-secret": { + "type": "text" + } + } + }, + "frameworkAssemblies": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "properties": { + "confluent-access-key": { + "type": "text" + }, + "rservicedapi-access-token": { + "type": "text" + }, + "sendgrid-service-secret": { + "type": "text" + }, + "slack-legacy-bot-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "datadog-access-key": { + "type": "text" + }, + "dropbox-api-token": { + "type": "text" + }, + "easypost-test-api-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "typeform-api-key": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "dropbox-long-lived-service-key": { + "type": "text" + }, + "messagebird-client-id": { + "type": "text" + }, + "new-relic-user-api-secret": { + "type": "text" + }, + "readme-service-secret": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "kucoin-token-key": { + "type": "text" + }, + "new-relic-user-service-id": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twilio-service-key": { + "type": "text" + }, + "type": { + "type": "text" + }, + "vault-service-secret": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "properties": { + "gitlab-pat": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "pulumi-service-secret": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + }, + "target": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_7": { + "type": "text" + }, + "additional_8": { + "type": "text" + }, + "bitbucket-client-id": { + "type": "text" + }, + "linkedin-client-key": { + "type": "text" + }, + "readme-api-token": { + "type": "text" + }, + "yandex-access-token": { + "type": "text" + } + } + }, + "jfrog-identity-key": { + "type": "text" + }, + "jwt": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + }, + "twitch-service-token": { + "type": "text" + } + } + }, + "additional_8": { + "properties": { + "beamer-service-secret": { + "type": "text" + }, + "bittrex-secret-token": { + "type": "text" + }, + "kraken-access-secret": { + "type": "text" + }, + "kucoin-key-secret": { + "type": "text" + } + } + }, + "gitlab-pat": { + "type": "text" + }, + "grafana-api-key": { + "type": "text" + }, + "rubygems-api-key": { + "type": "text" + }, + "shopify-custom-access-secret": { + "type": "text" + } + } + }, + "generic-api-token": { + "type": "text" + }, + "generic-service-token": { + "type": "text" + }, + "grafana-api-token": { + "type": "text" + }, + "grafana-cloud-service-secret": { + "type": "text" + }, + "guid": { + "type": "text" + }, + "icon": { + "type": "text" + }, + "imports": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "freshbooks-access-token": { + "type": "text" + }, + "shopify-access-token": { + "type": "text" + }, + "square-access-token": { + "type": "text" + }, + "stripe-access-secret": { + "type": "text" + } + } + }, + "invites_enabled": { + "type": "boolean" + }, + "ipfs_gateway_url": { + "type": "text" + }, + "jfrog-identity-key": { + "type": "text" + }, + "kraken-access-key": { + "type": "text" + }, + "languages": { + "type": "text" + }, + "launchdarkly-access-secret": { + "type": "text" + }, + "linear-service-token": { + "type": "text" + }, + "linkedin-client-key": { + "type": "text" + }, + "logLevel": { + "type": "long" + }, + "login_message": { + "type": "text" + }, + "mailgun-signing-key": { + "type": "text" + }, + "max_media_attachments": { + "type": "long" + }, + "max_post_chars": { + "type": "long" + }, + "max_toot_chars": { + "type": "long" + }, + "name": { + "type": "text" + }, + "new_accounts_read_only": { + "type": "boolean" + }, + "nostr": { + "properties": { + "pow": { + "properties": { + "registrations": { + "type": "long" + } + } + }, + "pubkey": { + "type": "text" + }, + "relay": { + "type": "text" + } + } + }, + "npm-access-secret": { + "type": "text" + }, + "object": { + "properties": { + "code": { + "type": "text" + }, + "name": { + "type": "text" + }, + "rel": { + "type": "text" + }, + "scheme": { + "type": "text" + } + } + }, + "packInclude": { + "properties": { + "easypost-api-key": { + "type": "text" + }, + "plaid-api-key": { + "type": "text" + }, + "planetscale-oauth-key": { + "type": "text" + }, + "vault-service-key": { + "type": "text" + } + } + }, + "pleroma": { + "properties": { + "favicon": { + "type": "text" + }, + "metadata": { + "properties": { + "account_activation_required": { + "type": "boolean" + }, + "birthday_min_age": { + "type": "long" + }, + "birthday_required": { + "type": "boolean" + }, + "features": { + "type": "text" + }, + "federation": { + "properties": { + "enabled": { + "type": "boolean" + }, + "exclusions": { + "type": "boolean" + }, + "mrf_bash_bot": { + "properties": { + "bot_account_nickname": { + "type": "text" + }, + "commands": { + "type": "text" + } + } + }, + "mrf_block_notification_policy": { + "type": "text" + }, + "mrf_emoji": { + "properties": { + "remove_shortcode": { + "type": "text" + } + } + }, + "mrf_hashtag": { + "properties": { + "federated_timeline_removal": { + "type": "text" + }, + "reject": { + "type": "text" + }, + "sensitive": { + "type": "text" + } + } + }, + "mrf_hellthread": { + "properties": { + "delist_threshold": { + "type": "long" + }, + "reject_threshold": { + "type": "long" + } + } + }, + "mrf_keyword": { + "properties": { + "federated_timeline_removal": { + "type": "text" + }, + "reject": { + "type": "text" + }, + "replace": { + "properties": { + "pattern": { + "type": "text" + }, + "replacement": { + "type": "text" + } + } + } + } + }, + "mrf_nsfw_api": { + "properties": { + "mark_sensitive": { + "type": "boolean" + }, + "reject": { + "type": "boolean" + }, + "threshold": { + "type": "float" + }, + "unlist": { + "type": "boolean" + } + } + }, + "mrf_object_age": { + "properties": { + "actions": { + "type": "text" + }, + "threshold": { + "type": "long" + } + } + }, + "mrf_policies": { + "type": "text" + }, + "mrf_rejectnonpublic": { + "properties": { + "allow_direct": { + "type": "boolean" + }, + "allow_followersonly": { + "type": "boolean" + } + } + }, + "mrf_remote_report": { + "properties": { + "reject_all": { + "type": "boolean" + }, + "reject_anonymous": { + "type": "boolean" + }, + "reject_empty_message": { + "type": "boolean" + } + } + }, + "mrf_sample": { + "properties": { + "content": { + "type": "text" + } + } + }, + "mrf_simple": { + "properties": { + "accept": { + "type": "text" + }, + "avatar_removal": { + "type": "text" + }, + "banner_removal": { + "type": "text" + }, + "federated_timeline_removal": { + "type": "text" + }, + "followers_only": { + "type": "text" + }, + "media_nsfw": { + "type": "text" + }, + "media_removal": { + "type": "text" + }, + "reject": { + "type": "text" + }, + "reject_deletes": { + "type": "text" + }, + "report_removal": { + "type": "text" + } + } + }, + "mrf_simple_info": { + "properties": { + "accept": { + "type": "object", + "enabled": false + }, + "avatar_removal": { + "type": "object", + "enabled": false + }, + "banner_removal": { + "type": "object", + "enabled": false + }, + "federated_timeline_removal": { + "type": "object", + "enabled": false + }, + "followers_only": { + "type": "object", + "enabled": false + }, + "media_nsfw": { + "type": "object", + "enabled": false + }, + "media_removal": { + "type": "object", + "enabled": false + }, + "reject": { + "type": "object", + "enabled": false + }, + "reject_deletes": { + "type": "object", + "enabled": false + }, + "report_removal": { + "type": "object", + "enabled": false + } + } + }, + "mrf_user_allowlist": { + "type": "object" + }, + "mrf_vocabulary": { + "type": "object" + }, + "quarantined_instances": { + "type": "object", + "enabled": false + }, + "quarantined_instances_info": { + "properties": { + "quarantined_instances": { + "type": "object", + "enabled": false + } + } + }, + "rejected_instances": { + "type": "object", + "enabled": false + } + } + }, + "fields_limits": { + "properties": { + "max_fields": { + "type": "long" + }, + "max_remote_fields": { + "type": "long" + }, + "name_length": { + "type": "long" + }, + "value_length": { + "type": "long" + } + } + }, + "markup": { + "properties": { + "allow_headings": { + "type": "boolean" + }, + "allow_inline_images": { + "type": "boolean" + }, + "allow_tables": { + "type": "boolean" + } + } + }, + "migration_cooldown_period": { + "type": "long" + }, + "post_formats": { + "type": "text" + }, + "privileged_staff": { + "type": "boolean" + }, + "restrict_unauthenticated": { + "properties": { + "activities": { + "properties": { + "local": { + "type": "boolean" + }, + "remote": { + "type": "boolean" + } + } + }, + "profiles": { + "properties": { + "local": { + "type": "boolean" + }, + "remote": { + "type": "boolean" + } + } + }, + "timelines": { + "properties": { + "federated": { + "type": "boolean" + }, + "local": { + "type": "boolean" + } + } + } + } + }, + "translation": { + "properties": { + "allow_remote": { + "type": "boolean" + }, + "allow_unauthenticated": { + "type": "boolean" + }, + "source_languages": { + "type": "text" + }, + "target_languages": { + "type": "text" + } + } + } + } + }, + "oauth_consumer_strategies": { + "type": "text" + }, + "stats": { + "properties": { + "mau": { + "type": "long" + } + } + }, + "vapid_public_key": { + "type": "text" + } + } + }, + "poll_limits": { + "properties": { + "max_expiration": { + "type": "double" + }, + "max_option_chars": { + "type": "long" + }, + "max_options": { + "type": "long" + }, + "min_expiration": { + "type": "double" + }, + "min_options": { + "type": "long" + } + } + }, + "postman-service-token": { + "type": "text" + }, + "profile": { + "type": "text" + }, + "profiles": { + "properties": { + "freshbooks-access-secret": { + "type": "text" + }, + "linkedin-client-secret": { + "type": "text" + }, + "mailchimp-api-secret": { + "type": "text" + }, + "twitter-service-token": { + "type": "text" + } + } + }, + "pulumi-service-secret": { + "type": "text" + }, + "readme-api-token": { + "type": "text" + }, + "registrations": { + "type": "boolean" + }, + "repository": { + "properties": { + "fastly-service-token": { + "type": "text" + }, + "finicity-client-token": { + "type": "text" + }, + "gitlab-ptt": { + "type": "text" + }, + "huggingface-organization-api-key": { + "type": "text" + } + } + }, + "requireLicenseAcceptance": { + "type": "boolean" + }, + "resource": { + "type": "text" + }, + "resourceFiles": { + "type": "text" + }, + "rubygems-service-token": { + "type": "text" + }, + "rules": { + "properties": { + "hint": { + "type": "text" + }, + "id": { + "type": "text" + }, + "text": { + "type": "text" + } + } + }, + "scenarios": { + "properties": { + "flow": { + "properties": { + "delete": { + "properties": { + "expect": { + "properties": { + "statusCode": { + "type": "long" + } + } + }, + "headers": { + "properties": { + "accept": { + "type": "text" + }, + "api_key": { + "type": "text" + }, + "contentType": { + "type": "text" + } + } + }, + "name": { + "type": "text" + }, + "url": { + "type": "text" + } + } + }, + "function": { + "type": "text" + }, + "get": { + "properties": { + "expect": { + "properties": { + "statusCode": { + "type": "long" + } + } + }, + "headers": { + "properties": { + "accept": { + "type": "text" + }, + "contentType": { + "type": "text" + } + } + }, + "name": { + "type": "text" + }, + "qs": { + "properties": { + "password": { + "type": "text" + }, + "username": { + "type": "text" + } + } + }, + "url": { + "type": "text" + } + } + }, + "post": { + "properties": { + "capture": { + "properties": { + "as": { + "type": "text" + }, + "json": { + "type": "text" + } + } + }, + "expect": { + "properties": { + "statusCode": { + "type": "long" + } + } + }, + "headers": { + "properties": { + "Content-Type": { + "type": "text" + }, + "accept": { + "type": "text" + } + } + }, + "json": { + "properties": { + "category": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "text" + } + } + }, + "email": { + "type": "text" + }, + "firstName": { + "type": "text" + }, + "id": { + "type": "long" + }, + "lastName": { + "type": "text" + }, + "name": { + "type": "text" + }, + "password": { + "type": "text" + }, + "phone": { + "type": "text" + }, + "photoUrls": { + "type": "text" + }, + "status": { + "type": "text" + }, + "tags": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "text" + } + } + }, + "userStatus": { + "type": "long" + }, + "username": { + "type": "text" + } + } + }, + "name": { + "type": "text" + }, + "url": { + "type": "text" + } + } + } + } + } + } + }, + "scopes": { + "properties": { + "additional_0": { + "properties": { + "beamer-api-secret": { + "type": "text" + }, + "jfrog-service-key": { + "type": "text" + }, + "private-secret": { + "type": "text" + }, + "twitter-service-secret": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "type": "text" + }, + "additional_7": { + "type": "text" + }, + "additional_8": { + "type": "text" + }, + "easypost-api-secret": { + "type": "text" + }, + "flutterwave-public-token": { + "type": "text" + }, + "huggingface-access-key": { + "type": "text" + }, + "new-relic-user-service-key": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "finnhub-access-secret": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "grafana-service-account-secret": { + "type": "text" + }, + "shopify-private-app-access-secret": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "type": "text" + }, + "additional_7": { + "type": "text" + }, + "mailgun-pub-token": { + "type": "text" + }, + "square-access-secret": { + "type": "text" + }, + "twitter-api-secret": { + "type": "text" + }, + "yandex-aws-access-secret": { + "type": "text" + } + } + }, + "easypost-test-api-key": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "mailgun-private-service-secret": { + "type": "text" + } + } + }, + "sendbird-access-token": { + "type": "text" + }, + "sentry-access-secret": { + "type": "text" + }, + "shared": { + "type": "text" + }, + "shopify-shared-secret": { + "type": "text" + }, + "short_description": { + "type": "text" + }, + "short_description_text": { + "type": "text" + }, + "shout_limit": { + "type": "long" + }, + "sidekiq-sensitive-url": { + "type": "text" + }, + "slack-config-access-secret": { + "type": "text" + }, + "slack-config-refresh-key": { + "type": "text" + }, + "slack-legacy-workspace-token": { + "type": "text" + }, + "snyk-service-key": { + "type": "text" + }, + "snyk-service-secret": { + "type": "text" + }, + "soapbox": { + "properties": { + "version": { + "type": "text" + } + } + }, + "software_name": { + "type": "text" + }, + "software_version": { + "type": "text" + }, + "source_url": { + "type": "text" + }, + "stats": { + "properties": { + "domain_count": { + "type": "integer" + }, + "remote_user_count": { + "type": "integer" + }, + "status_count": { + "type": "integer" + }, + "user_count": { + "type": "integer" + } + } + }, + "strictSSL": { + "type": "boolean" + }, + "subject": { + "properties": { + "code": { + "type": "text" + }, + "name": { + "type": "text" + }, + "rel": { + "type": "text" + }, + "scheme": { + "type": "text" + } + } + }, + "sumologic-access-key": { + "type": "text" + }, + "tags": { + "type": "text" + }, + "terms": { + "type": "text" + }, + "terms_text": { + "type": "text" + }, + "thumbnail": { + "type": "text" + }, + "thumbnail_description": { + "type": "text" + }, + "thumbnail_static": { + "type": "text" + }, + "thumbnail_static_type": { + "type": "text" + }, + "thumbnail_type": { + "type": "text" + }, + "title": { + "type": "text" + }, + "token": { + "type": "text" + }, + "trustindicator": { + "properties": { + "code": { + "type": "text" + } + } + }, + "twitter-service-secret": { + "type": "text" + }, + "upload_limit": { + "type": "long" + }, + "uri": { + "type": "text" + }, + "urls": { + "properties": { + "streaming_api": { + "type": "text" + } + } + }, + "version": { + "type": "text" + }, + "wordmark": { + "type": "text" + }, + "wordmark_dark": { + "type": "text" + }, + "yandex-service-token": { + "type": "text" + } + } + }, + "blocks": { + "type": "nested", + "properties": { + "accountId": { + "type": "keyword" + }, + "accountUid": { + "type": "keyword" + }, + "additional_public_assets": { + "type": "text" + }, + "adobe-client-secret": { + "type": "text" + }, + "algolia-service-token": { + "type": "text" + }, + "aliases": { + "type": "text" + }, + "asset_name": { + "type": "text" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "adafruit-service-secret": { + "type": "text" + }, + "builder_factories": { + "type": "text" + }, + "defaults": { + "properties": { + "defined-networking-api-token": { + "type": "text" + }, + "dev_options": { + "properties": { + "grafana-api-account-secret": { + "type": "text" + }, + "hubspot-api-token": { + "type": "text" + }, + "mapbox-api-key": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + } + } + }, + "digitalocean-refresh-secret": { + "type": "text" + }, + "finicity-client-secret": { + "type": "text" + }, + "openai-api-key": { + "type": "text" + }, + "options": { + "properties": { + "easypost-test-service-secret": { + "type": "text" + }, + "gitter-access-secret": { + "type": "text" + }, + "huggingface-organization-service-token": { + "type": "text" + }, + "private-key": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "dynatrace-api-token": { + "type": "text" + }, + "etsy-access-secret": { + "type": "text" + }, + "finicity-service-token": { + "type": "text" + }, + "freshbooks-access-key": { + "type": "text" + } + } + } + } + }, + "grafana-api-secret": { + "type": "text" + }, + "hubspot-service-secret": { + "type": "text" + }, + "twitch-api-key": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "builder_factories": { + "type": "text" + }, + "finnhub-access-secret": { + "type": "text" + }, + "is_optional": { + "type": "boolean" + }, + "mapbox-api-secret": { + "type": "text" + }, + "runs_before": { + "type": "text" + }, + "sumologic-access-key": { + "type": "text" + }, + "target": { + "type": "text" + }, + "twitter-service-key": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "build_to": { + "type": "text" + }, + "builder_factories": { + "type": "text" + }, + "okta-access-secret": { + "type": "text" + }, + "readme-api-token": { + "type": "text" + }, + "shopify-private-app-access-key": { + "type": "text" + }, + "twitter-bearer-secret": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "applies_builders": { + "type": "text" + }, + "auto_apply": { + "type": "text" + }, + "build_extensions": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "discord-api-token": { + "type": "text" + }, + "mapbox-service-token": { + "type": "text" + }, + "openai-api-secret": { + "type": "text" + }, + "slack-user-secret": { + "type": "text" + } + } + }, + "doppler-api-key": { + "type": "text" + }, + "freshbooks-access-token": { + "type": "text" + }, + "planetscale-password": { + "type": "text" + }, + "rubygems-api-token": { + "type": "text" + }, + "runs_before": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "mailchimp-api-key": { + "type": "text" + }, + "netlify-access-token": { + "type": "text" + }, + "pulumi-service-key": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + }, + "target": { + "type": "text" + } + } + }, + "additional_5": { + "properties": { + "auto_apply": { + "type": "text" + }, + "build_extensions": { + "properties": { + "clojars-service-key": { + "type": "text" + }, + "facebook": { + "type": "text" + }, + "gcp-api-token": { + "type": "text" + }, + "linear-api-secret": { + "type": "text" + } + } + }, + "builder_factories": { + "type": "text" + }, + "finicity-client-secret": { + "type": "text" + }, + "flutterwave-public-token": { + "type": "text" + }, + "is_optional": { + "type": "boolean" + }, + "shopify-custom-access-key": { + "type": "text" + }, + "twitter-api-secret": { + "type": "text" + } + } + }, + "mailchimp-api-secret": { + "type": "text" + }, + "shopify-shared-token": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + } + } + }, + "channelNo": { + "type": "long" + }, + "channelType": { + "type": "keyword" + }, + "coinbase-access-token": { + "type": "text" + }, + "comment": { + "type": "keyword" + }, + "community_bridge": { + "type": "text" + }, + "contentful-delivery-api-secret": { + "type": "text" + }, + "crates": { + "properties": { + "deps": { + "properties": { + "crate": { + "type": "long" + }, + "name": { + "type": "text" + } + } + }, + "display_name": { + "type": "text" + }, + "edition": { + "type": "text" + }, + "is_proc_macro": { + "type": "boolean" + }, + "is_workspace_member": { + "type": "boolean" + }, + "repository": { + "type": "text" + }, + "root_module": { + "type": "text" + }, + "target": { + "type": "text" + } + } + }, + "created_at": { + "type": "date" + }, + "dependencies": { + "properties": { + "additional_0": { + "properties": { + "droneci-access-key": { + "type": "text" + }, + "lob-service-key": { + "type": "text" + }, + "plaid-key-token": { + "type": "text" + }, + "twilio-api-key": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "doppler-service-secret": { + "type": "text" + }, + "github-fine-grained-pat": { + "type": "text" + }, + "grafana-service-account-secret": { + "type": "text" + }, + "planetscale-password": { + "type": "text" + }, + "type": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "clojars-api-key": { + "type": "text" + }, + "easypost-api-secret": { + "type": "text" + }, + "slack-user-key": { + "type": "text" + }, + "yandex-service-secret": { + "type": "text" + } + } + }, + "description": { + "properties": { + "color": { + "type": "text" + }, + "confluent-access-secret": { + "type": "text" + }, + "github-pat": { + "type": "text" + }, + "linkedin-client-token": { + "type": "text" + }, + "sumologic-access-id": { + "type": "text" + }, + "translate": { + "type": "text" + } + } + }, + "digest": { + "type": "keyword" + }, + "digitalocean-pat": { + "type": "text" + }, + "digitalocean-refresh-token": { + "type": "text" + }, + "domain": { + "type": "keyword" + }, + "droneci-access-secret": { + "type": "text" + }, + "dropbox-short-lived-service-key": { + "type": "text" + }, + "dynatrace-service-token": { + "type": "text" + }, + "exceptionLogging": { + "type": "text" + }, + "expires": { + "type": "date" + }, + "fields": { + "type": "text" + }, + "finicity-client-token": { + "type": "text" + }, + "finnhub-access-secret": { + "type": "text" + }, + "flutterwave-token-key": { + "type": "text" + }, + "generic-service-token": { + "type": "text" + }, + "github-fine-grained-pat": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "global_options": { + "properties": { + "additional_0": { + "properties": { + "dev_options": { + "properties": { + "atlassian-api-key": { + "type": "text" + }, + "authress-service-client-access-secret": { + "type": "text" + }, + "sendgrid-service-secret": { + "type": "text" + }, + "twilio-service-token": { + "type": "text" + } + } + }, + "finicity-client-key": { + "type": "text" + }, + "gocardless-api-key": { + "type": "text" + }, + "kucoin-access-secret": { + "type": "text" + }, + "options": { + "properties": { + "bitbucket-client-id": { + "type": "text" + }, + "discord-client-id": { + "type": "text" + }, + "freshbooks-access-token": { + "type": "text" + }, + "twitter-service-key": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "alibaba-access-secret-id": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "linkedin-client-token": { + "type": "text" + }, + "mailgun-private-api-key": { + "type": "text" + } + } + }, + "telegram-bot-api-key": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "dev_options": { + "properties": { + "github-app-key": { + "type": "text" + }, + "hashicorp-tf-service-token": { + "type": "text" + }, + "huggingface-organization-service-key": { + "type": "text" + }, + "yandex-service-key": { + "type": "text" + } + } + }, + "gcp-api-secret": { + "type": "text" + }, + "grafana-service-account-token": { + "type": "text" + }, + "linkedin-client-key": { + "type": "text" + }, + "options": { + "properties": { + "alibaba-token-token": { + "type": "text" + }, + "finicity-api-token": { + "type": "text" + }, + "sendbird-access-id": { + "type": "text" + }, + "sendgrid-api-token": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "bittrex-key-secret": { + "type": "text" + }, + "fastly-api-key": { + "type": "text" + }, + "gcp-service-token": { + "type": "text" + }, + "rubygems-api-key": { + "type": "text" + } + } + }, + "runs_before": { + "type": "text" + }, + "yandex-access-token": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "dev_options": { + "properties": { + "github-refresh-key": { + "type": "text" + }, + "jfrog-api-secret": { + "type": "text" + }, + "linear-service-token": { + "type": "text" + }, + "linkedin-client-secret": { + "type": "text" + } + } + }, + "huggingface-access-key": { + "type": "text" + }, + "options": { + "properties": { + "databricks-api-key": { + "type": "text" + }, + "defined-networking-service-token": { + "type": "text" + }, + "digitalocean-refresh-key": { + "type": "text" + }, + "pulumi-api-secret": { + "type": "text" + } + } + }, + "plaid-token-key": { + "type": "text" + }, + "release_options": { + "properties": { + "flickr-access-token": { + "type": "text" + }, + "grafana-cloud-api-key": { + "type": "text" + }, + "mapbox-service-token": { + "type": "text" + }, + "readme-service-key": { + "type": "text" + } + } + }, + "scalingo-api-secret": { + "type": "text" + }, + "square-access-token": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "beamer-api-key": { + "type": "text" + }, + "github-oauth": { + "type": "text" + }, + "mailgun-pub-key": { + "type": "text" + }, + "options": { + "properties": { + "adobe-client-id": { + "type": "text" + }, + "beamer-api-token": { + "type": "text" + }, + "gitlab-ptt": { + "type": "text" + }, + "scalingo-api-token": { + "type": "text" + } + } + }, + "runs_before": { + "type": "text" + }, + "twitter-bearer-secret": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "dev_options": { + "properties": { + "bittrex-access-key": { + "type": "text" + }, + "doppler-service-token": { + "type": "text" + }, + "kucoin-access-key": { + "type": "text" + }, + "mailgun-private-api-secret": { + "type": "text" + } + } + }, + "fastly-api-secret": { + "type": "text" + }, + "hashicorp-tf-service-key": { + "type": "text" + }, + "huggingface-access-key": { + "type": "text" + }, + "lob-pub-api-token": { + "type": "text" + }, + "options": { + "properties": { + "contentful-delivery-service-token": { + "type": "text" + }, + "intercom-api-token": { + "type": "text" + }, + "launchdarkly-access-secret": { + "type": "text" + }, + "linear-client-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "clojars-api-token": { + "type": "text" + }, + "linkedin-client-token": { + "type": "text" + }, + "rubygems-api-key": { + "type": "text" + }, + "yandex-api-key": { + "type": "text" + } + } + } + } + }, + "additional_5": { + "properties": { + "alibaba-secret-key": { + "type": "text" + }, + "dev_options": { + "properties": { + "github-refresh-secret": { + "type": "text" + }, + "private-secret": { + "type": "text" + }, + "rservicedapi-access-secret": { + "type": "text" + }, + "slack-app-secret": { + "type": "text" + } + } + }, + "lob-pub-api-token": { + "type": "text" + }, + "npm-access-secret": { + "type": "text" + }, + "options": { + "properties": { + "discord-client-key": { + "type": "text" + }, + "nytimes-access-token": { + "type": "text" + }, + "scalingo-api-token": { + "type": "text" + }, + "slack-legacy-workspace-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "bittrex-token-key": { + "type": "text" + }, + "easypost-test-api-secret": { + "type": "text" + }, + "hashicorp-tf-service-key": { + "type": "text" + }, + "zendesk-key-secret": { + "type": "text" + } + } + }, + "runs_before": { + "type": "text" + }, + "slack-legacy-workspace-key": { + "type": "text" + } + } + }, + "additional_6": { + "properties": { + "airtable-api-key": { + "type": "text" + }, + "shopify-custom-access-key": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + }, + "vault-api-key": { + "type": "text" + } + } + }, + "algolia-service-token": { + "type": "text" + }, + "bittrex-key-key": { + "type": "text" + }, + "digitalocean-refresh-token": { + "type": "text" + }, + "flickr-access-secret": { + "type": "text" + }, + "gcp-service-key": { + "type": "text" + }, + "slack-user-secret": { + "type": "text" + }, + "sumologic-access-secret": { + "type": "text" + }, + "yandex-access-key": { + "type": "text" + } + } + }, + "grade": { + "type": "keyword" + }, + "grafana-service-account-token": { + "type": "text" + }, + "heroku-api-key": { + "type": "text" + }, + "heroku-service-secret": { + "type": "text" + }, + "hubspot-api-secret": { + "type": "text" + }, + "id": { + "type": "integer" + }, + "ingredient": { + "type": "text" + }, + "item_model_index": { + "type": "float" + }, + "jfrog-api-secret": { + "type": "text" + }, + "kucoin-access-key": { + "type": "text" + }, + "launchdarkly-access-key": { + "type": "text" + }, + "linear-client-token": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "linkedin-client-token": { + "type": "text" + }, + "links": { + "properties": { + "href": { + "type": "text" + }, + "properties": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "type": "text" + }, + "additional_8": { + "type": "text" + } + } + }, + "rel": { + "type": "text" + }, + "template": { + "type": "text" + }, + "titles": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "additional_3": { + "type": "text" + }, + "additional_4": { + "type": "text" + }, + "additional_5": { + "type": "text" + }, + "additional_6": { + "type": "text" + }, + "additional_7": { + "type": "text" + }, + "default": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "name": { + "type": "keyword" + }, + "new-relic-user-api-id": { + "type": "text" + }, + "new-relic-user-api-key": { + "type": "text" + }, + "nytimes-access-key": { + "type": "text" + }, + "openai-service-key": { + "type": "text" + }, + "owners": { + "properties": { + "email": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "patreon": { + "type": "text" + }, + "plaid-api-token": { + "type": "text" + }, + "planetscale-api-key": { + "type": "text" + }, + "planetscale-api-secret": { + "type": "text" + }, + "planetscale-oauth-token": { + "type": "text" + }, + "project": { + "properties": { + "gitlab-pat": { + "type": "text" + }, + "jwt": { + "type": "text" + }, + "name": { + "type": "text" + }, + "settings": { + "type": "text" + }, + "twitter-access-secret": { + "type": "text" + }, + "yandex-access-token": { + "type": "text" + } + } + }, + "properties": { + "properties": { + "additional_0": { + "type": "text" + }, + "additional_1": { + "type": "text" + }, + "additional_2": { + "type": "text" + }, + "bittrex-access-token": { + "type": "text" + }, + "flutterwave-secret-token": { + "type": "text" + }, + "new-relic-browser-service-key": { + "type": "text" + }, + "shopify-access-token": { + "type": "text" + } + } + }, + "repositoryID": { + "type": "text" + }, + "representativeImageUrl": { + "type": "keyword" + }, + "rubygems-service-key": { + "type": "text" + }, + "rules": { + "properties": { + "allowData": { + "type": "boolean" + }, + "path": { + "type": "text" + }, + "regex": { + "type": "text" + }, + "types": { + "type": "text" + } + } + }, + "sendbird-access-id": { + "type": "text" + }, + "sentry-access-key": { + "type": "text" + }, + "severity": { + "type": "keyword" + }, + "severity_ex": { + "type": "keyword" + }, + "shippo-api-secret": { + "type": "text" + }, + "shopify-custom-access-key": { + "type": "text" + }, + "shopify-custom-access-secret": { + "type": "text" + }, + "sidekiq-secret": { + "type": "text" + }, + "slack-bot-token": { + "type": "text" + }, + "slack-config-access-token": { + "type": "text" + }, + "slack-config-refresh-secret": { + "type": "text" + }, + "slack-legacy-workspace-key": { + "type": "text" + }, + "solution": { + "properties": { + "linear-api-key": { + "type": "text" + }, + "path": { + "type": "text" + }, + "postman-api-token": { + "type": "text" + }, + "projects": { + "type": "text" + }, + "pulumi-api-secret": { + "type": "text" + }, + "sentry-access-secret": { + "type": "text" + } + } + }, + "stages": { + "properties": { + "5wo19kq0 7BnVPMLpu kBu 7D6LpVh3dK AkqIgQo Kd hG6K a2Nz": { + "properties": { + "age key token": { + "type": "text" + }, + "grafana-api-account-token": { + "type": "text" + }, + "slack-config-refresh-secret": { + "type": "text" + }, + "stripe-access-key": { + "type": "text" + }, + "when": { + "properties": { + "branch": { + "type": "text" + }, + "gitlab-rrt": { + "type": "text" + }, + "mailgun-pub-token": { + "type": "text" + }, + "slack-app-token": { + "type": "text" + }, + "twitter-access-key": { + "type": "text" + } + } + } + } + }, + "PaD9J xHqx7 jSYSTOUA sL6 vncImsPf bgx": { + "properties": { + "flutterwave-token-token": { + "type": "text" + }, + "huggingface-organization-service-secret": { + "type": "text" + }, + "huggingface-organization-service-token": { + "type": "text" + }, + "sendbird-access-id": { + "type": "text" + } + } + }, + "duffel-api-token": { + "type": "text" + }, + "sentry-access-key": { + "type": "text" + }, + "travisci-access-key": { + "type": "text" + }, + "yandex-aws-access-token": { + "type": "text" + } + } + }, + "stock_auto": { + "type": "boolean" + }, + "stripe-access-secret": { + "type": "text" + }, + "subject": { + "type": "text" + }, + "sumologic-access-token": { + "type": "text" + }, + "targets": { + "properties": { + "additional_0": { + "properties": { + "atlassian-api-key": { + "type": "text" + }, + "auto_apply_builders": { + "type": "boolean" + }, + "bitbucket-client-key": { + "type": "text" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "confluent-key-secret": { + "type": "text" + }, + "dev_options": { + "properties": { + "flutterwave-key-token": { + "type": "text" + }, + "scalingo-service-token": { + "type": "text" + }, + "slack-legacy-workspace-secret": { + "type": "text" + }, + "square-access-secret": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "jwt-base64": { + "type": "text" + }, + "options": { + "properties": { + "airtable-service-key": { + "type": "text" + }, + "datadog-access-secret": { + "type": "text" + }, + "gocardless-service-secret": { + "type": "text" + }, + "private-secret": { + "type": "text" + } + } + }, + "plaid-service-token": { + "type": "text" + }, + "release_options": { + "properties": { + "clojars-service-secret": { + "type": "text" + }, + "github-pat": { + "type": "text" + }, + "launchdarkly-access-key": { + "type": "text" + }, + "nytimes-access-secret": { + "type": "text" + } + } + }, + "sendgrid-service-secret": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "asana-client-id": { + "type": "text" + }, + "dev_options": { + "properties": { + "atlassian-api-secret": { + "type": "text" + }, + "contentful-delivery-service-key": { + "type": "text" + }, + "sendbird-access-id": { + "type": "text" + }, + "yandex-service-key": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "npm-access-token": { + "type": "text" + }, + "options": { + "properties": { + "github-refresh-secret": { + "type": "text" + }, + "rapidapi-access-token": { + "type": "text" + }, + "slack-legacy-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "dropbox-long-lived-service-token": { + "type": "text" + }, + "flickr-access-token": { + "type": "text" + }, + "rapidservice-access-key": { + "type": "text" + }, + "typeform-api-key": { + "type": "text" + } + } + }, + "shopify-access-token": { + "type": "text" + }, + "telegram-bot-service-key": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "defined-networking-service-secret": { + "type": "text" + }, + "sentry-access-key": { + "type": "text" + }, + "slack-legacy-bot-token": { + "type": "text" + }, + "twitch-api-token": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "authress-api-client-access-secret": { + "type": "text" + }, + "codecov-access-key": { + "type": "text" + }, + "dev_options": { + "properties": { + "digitalocean-refresh-key": { + "type": "text" + }, + "jwt": { + "type": "text" + }, + "mapbox-api-token": { + "type": "text" + }, + "shopify-shared-secret": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "generate_for": { + "properties": { + "alibaba-key-token": { + "type": "text" + }, + "digitalocean-pat": { + "type": "text" + }, + "dynatrace-service-token": { + "type": "text" + }, + "squarespace-access-token": { + "type": "text" + } + } + }, + "microsoft-teams-webhook": { + "type": "text" + }, + "options": { + "properties": { + "algolia-api-token": { + "type": "text" + }, + "asana-client-id": { + "type": "text" + }, + "messagebird-service-token": { + "type": "text" + }, + "shopify-access-token": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "digitalocean-refresh-secret": { + "type": "text" + }, + "gcp-service-secret": { + "type": "text" + }, + "plaid-api-key": { + "type": "text" + }, + "planetscale-service-secret": { + "type": "text" + } + } + }, + "slack-legacy-bot-token": { + "type": "text" + } + } + }, + "clojars-service-key": { + "type": "text" + }, + "finicity-client-token": { + "type": "text" + }, + "linear-api-token": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + } + } + }, + "dependencies": { + "type": "text" + }, + "linear-service-key": { + "type": "text" + }, + "new-relic-browser-api-secret": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + }, + "readme-service-key": { + "type": "text" + }, + "sources": { + "properties": { + "droneci-access-key": { + "type": "text" + }, + "flickr-access-secret": { + "type": "text" + }, + "hubspot-service-key": { + "type": "text" + }, + "launchdarkly-access-token": { + "type": "text" + }, + "mailchimp-service-token": { + "type": "text" + }, + "mapbox-service-secret": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + }, + "typeform-service-secret": { + "type": "text" + } + } + }, + "twitter-service-secret": { + "type": "text" + }, + "yandex-api-key": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "beamer-api-secret": { + "type": "text" + }, + "dependencies": { + "type": "text" + }, + "discord-client-key": { + "type": "text" + }, + "easypost-test-api-token": { + "type": "text" + }, + "gitlab-rrt": { + "type": "text" + }, + "jwt-base64": { + "type": "text" + }, + "planetscale-password": { + "type": "text" + }, + "slack-config-refresh-token": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + }, + "sources": { + "properties": { + "digitalocean-refresh-secret": { + "type": "text" + }, + "flutterwave-encryption-key": { + "type": "text" + }, + "gitter-access-key": { + "type": "text" + }, + "twitter-bearer-key": { + "type": "text" + } + } + } + } + }, + "additional_2": { + "properties": { + "adafruit-api-token": { + "type": "text" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "dev_options": { + "properties": { + "age key token": { + "type": "text" + }, + "jfrog-identity-key": { + "type": "text" + }, + "jfrog-identity-token": { + "type": "text" + }, + "pulumi-api-token": { + "type": "text" + } + } + }, + "duffel-service-key": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "generate_for": { + "properties": { + "datadog-access-key": { + "type": "text" + }, + "dropbox-long-lived-service-secret": { + "type": "text" + }, + "finnhub-access-secret": { + "type": "text" + }, + "github-pat": { + "type": "text" + }, + "planetscale-api-token": { + "type": "text" + }, + "shopify-custom-access-key": { + "type": "text" + }, + "sidekiq-token": { + "type": "text" + }, + "slack-legacy-workspace-secret": { + "type": "text" + } + } + }, + "github-pat": { + "type": "text" + }, + "options": { + "properties": { + "adobe-client-token": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + }, + "sumologic-access-token": { + "type": "text" + } + } + }, + "planetscale-service-secret": { + "type": "text" + }, + "pulumi-api-token": { + "type": "text" + }, + "readme-api-token": { + "type": "text" + }, + "release_options": { + "properties": { + "linear-client-key": { + "type": "text" + }, + "postman-api-token": { + "type": "text" + }, + "shopify-private-app-access-key": { + "type": "text" + }, + "squarespace-access-key": { + "type": "text" + } + } + }, + "slack-legacy-bot-token": { + "type": "text" + }, + "sumologic-access-key": { + "type": "text" + }, + "twitter-access-secret": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "datadog-access-key": { + "type": "text" + }, + "dev_options": { + "properties": { + "frameio-service-key": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "new-relic-user-api-token": { + "type": "text" + }, + "pulumi-service-token": { + "type": "text" + } + } + }, + "digitalocean-access-token": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "jfrog-api-token": { + "type": "text" + }, + "launchdarkly-access-secret": { + "type": "text" + }, + "options": { + "properties": { + "adobe-client-id": { + "type": "text" + }, + "finicity-client-secret": { + "type": "text" + }, + "gitlab-rrt": { + "type": "text" + }, + "rubygems-service-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "authress-api-client-access-token": { + "type": "text" + }, + "freshbooks-access-secret": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + }, + "shopify-private-app-access-secret": { + "type": "text" + } + } + }, + "shopify-access-key": { + "type": "text" + }, + "shopify-custom-access-key": { + "type": "text" + }, + "shopify-private-app-access-token": { + "type": "text" + }, + "stripe-access-token": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "dev_options": { + "properties": { + "atlassian-service-secret": { + "type": "text" + }, + "droneci-access-key": { + "type": "text" + }, + "etsy-access-token": { + "type": "text" + }, + "fastly-api-token": { + "type": "text" + } + } + }, + "easypost-test-api-key": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "flutterwave-encryption-token": { + "type": "text" + }, + "mailgun-signing-key": { + "type": "text" + }, + "options": { + "properties": { + "asana-client-token": { + "type": "text" + }, + "github-app-token": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + }, + "sumologic-access-token": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "asana-client-secret": { + "type": "text" + }, + "discord-client-key": { + "type": "text" + }, + "messagebird-client-id": { + "type": "text" + }, + "scalingo-api-secret": { + "type": "text" + } + } + }, + "shopify-custom-access-key": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "dev_options": { + "properties": { + "defined-networking-service-key": { + "type": "text" + }, + "etsy-access-secret": { + "type": "text" + }, + "openai-api-key": { + "type": "text" + }, + "twitter-access-secret": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "etsy-access-secret": { + "type": "text" + }, + "finicity-service-token": { + "type": "text" + }, + "generate_for": { + "properties": { + "age secret secret": { + "type": "text" + }, + "authress-service-client-access-key": { + "type": "text" + }, + "discord-service-token": { + "type": "text" + }, + "npm-access-key": { + "type": "text" + } + } + }, + "grafana-cloud-service-token": { + "type": "text" + }, + "options": { + "properties": { + "contentful-delivery-api-key": { + "type": "text" + }, + "github-app-key": { + "type": "text" + }, + "gocardless-service-secret": { + "type": "text" + }, + "shippo-service-token": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "alibaba-token-token": { + "type": "text" + }, + "github-refresh-key": { + "type": "text" + }, + "gocardless-api-token": { + "type": "text" + }, + "yandex-access-secret": { + "type": "text" + } + } + }, + "sendbird-access-id": { + "type": "text" + } + } + }, + "clojars-service-secret": { + "type": "text" + }, + "droneci-access-key": { + "type": "text" + }, + "easypost-test-api-token": { + "type": "text" + }, + "facebook": { + "type": "text" + }, + "linkedin-client-id": { + "type": "text" + }, + "messagebird-client-id": { + "type": "text" + }, + "slack-legacy-secret": { + "type": "text" + }, + "square-access-secret": { + "type": "text" + } + } + }, + "dependencies": { + "type": "text" + }, + "github-app-secret": { + "type": "text" + }, + "github-pat": { + "type": "text" + }, + "gitlab-pat": { + "type": "text" + }, + "jfrog-identity-secret": { + "type": "text" + }, + "mattermost-access-secret": { + "type": "text" + }, + "sidekiq-secret": { + "type": "text" + }, + "sources": { + "properties": { + "airtable-service-key": { + "type": "text" + }, + "flickr-access-token": { + "type": "text" + }, + "planetscale-api-key": { + "type": "text" + }, + "slack-user-key": { + "type": "text" + } + } + }, + "zendesk-secret-secret": { + "type": "text" + } + } + }, + "additional_3": { + "properties": { + "auto_apply_builders": { + "type": "boolean" + }, + "aws-access-token": { + "type": "text" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "alibaba-access-key-id": { + "type": "text" + }, + "dev_options": { + "properties": { + "adobe-client-id": { + "type": "text" + }, + "fastly-api-secret": { + "type": "text" + }, + "grafana-cloud-service-token": { + "type": "text" + }, + "huggingface-access-token": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "generate_for": { + "properties": { + "algolia-service-token": { + "type": "text" + }, + "bittrex-access-token": { + "type": "text" + }, + "flickr-access-secret": { + "type": "text" + }, + "shippo-service-secret": { + "type": "text" + } + } + }, + "hubspot-service-token": { + "type": "text" + }, + "options": { + "properties": { + "adafruit-service-secret": { + "type": "text" + }, + "authress-api-client-access-secret": { + "type": "text" + }, + "digitalocean-pat": { + "type": "text" + }, + "openai-service-key": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "nytimes-access-token": { + "type": "text" + }, + "twitter-api-token": { + "type": "text" + }, + "zendesk-token-key": { + "type": "text" + } + } + }, + "twilio-api-token": { + "type": "text" + }, + "twitter-access-key": { + "type": "text" + } + } + }, + "additional_1": { + "properties": { + "dev_options": { + "properties": { + "beamer-service-key": { + "type": "text" + }, + "hubspot-service-token": { + "type": "text" + }, + "linear-client-token": { + "type": "text" + }, + "pulumi-api-secret": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "finnhub-access-secret": { + "type": "text" + }, + "jfrog-identity-key": { + "type": "text" + }, + "okta-access-key": { + "type": "text" + }, + "options": { + "properties": { + "adobe-client-secret": { + "type": "text" + }, + "gcp-api-secret": { + "type": "text" + }, + "mailgun-signing-key": { + "type": "text" + }, + "slack-legacy-workspace-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "adobe-client-key": { + "type": "text" + }, + "dropbox-api-secret": { + "type": "text" + }, + "shippo-api-token": { + "type": "text" + }, + "slack-config-access-secret": { + "type": "text" + } + } + }, + "shopify-access-token": { + "type": "text" + } + } + }, + "additional_2": { + "properties": { + "bittrex-key-secret": { + "type": "text" + }, + "doppler-service-secret": { + "type": "text" + }, + "fastly-api-secret": { + "type": "text" + }, + "generate_for": { + "properties": { + "fastly-api-secret": { + "type": "text" + }, + "messagebird-client-id": { + "type": "text" + }, + "slack-config-access-key": { + "type": "text" + }, + "slack-webhook-url": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "adobe-client-id": { + "type": "text" + }, + "heroku-service-token": { + "type": "text" + }, + "pypi-upload-token": { + "type": "text" + }, + "vault-batch-token": { + "type": "text" + } + } + }, + "yandex-aws-access-key": { + "type": "text" + } + } + }, + "digitalocean-access-key": { + "type": "text" + }, + "finnhub-access-key": { + "type": "text" + }, + "flutterwave-encryption-secret": { + "type": "text" + }, + "github-oauth": { + "type": "text" + } + } + }, + "dependencies": { + "type": "text" + }, + "huggingface-organization-service-token": { + "type": "text" + }, + "slack-config-access-secret": { + "type": "text" + }, + "twitter-api-token": { + "type": "text" + } + } + }, + "additional_4": { + "properties": { + "auto_apply_builders": { + "type": "boolean" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "asana-client-id": { + "type": "text" + }, + "dev_options": { + "properties": { + "bittrex-key-secret": { + "type": "text" + }, + "clojars-api-secret": { + "type": "text" + }, + "gitlab-rrt": { + "type": "text" + }, + "snyk-service-key": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "grafana-cloud-service-key": { + "type": "text" + }, + "new-relic-user-service-id": { + "type": "text" + }, + "options": { + "properties": { + "adafruit-service-secret": { + "type": "text" + }, + "databricks-service-token": { + "type": "text" + }, + "mailgun-pub-token": { + "type": "text" + }, + "slack-bot-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "jfrog-api-key": { + "type": "text" + }, + "mailchimp-service-secret": { + "type": "text" + }, + "sidekiq-secret": { + "type": "text" + }, + "twitter-api-key": { + "type": "text" + } + } + }, + "slack-legacy-bot-secret": { + "type": "text" + } + } + }, + "age key secret": { + "type": "text" + }, + "gitlab-ptt": { + "type": "text" + }, + "linear-api-token": { + "type": "text" + }, + "new-relic-user-service-id": { + "type": "text" + } + } + }, + "dependencies": { + "type": "text" + }, + "droneci-access-key": { + "type": "text" + }, + "dropbox-short-lived-service-key": { + "type": "text" + }, + "microsoft-teams-webhook": { + "type": "text" + }, + "slack-bot-token": { + "type": "text" + } + } + }, + "additional_5": { + "properties": { + "auto_apply_builders": { + "type": "boolean" + }, + "aws-access-key": { + "type": "text" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "confluent-secret-key": { + "type": "text" + }, + "dev_options": { + "properties": { + "etsy-access-key": { + "type": "text" + }, + "flickr-access-key": { + "type": "text" + }, + "new-relic-user-service-secret": { + "type": "text" + }, + "planetscale-password": { + "type": "text" + } + } + }, + "flutterwave-key-secret": { + "type": "text" + }, + "nytimes-access-token": { + "type": "text" + }, + "options": { + "properties": { + "finicity-api-key": { + "type": "text" + }, + "lob-pub-api-key": { + "type": "text" + }, + "mapbox-api-key": { + "type": "text" + }, + "planetscale-oauth-key": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "atlassian-service-token": { + "type": "text" + }, + "aws-access-secret": { + "type": "text" + }, + "sendbird-access-key": { + "type": "text" + }, + "yandex-service-key": { + "type": "text" + } + } + }, + "twitter-bearer-secret": { + "type": "text" + } + } + }, + "github-refresh-key": { + "type": "text" + }, + "gitlab-rrt": { + "type": "text" + }, + "new-relic-user-api-id": { + "type": "text" + }, + "sendbird-access-token": { + "type": "text" + } + } + }, + "dependencies": { + "type": "text" + }, + "flutterwave-key-key": { + "type": "text" + }, + "github-fine-grained-pat": { + "type": "text" + }, + "sources": { + "properties": { + "adafruit-api-token": { + "type": "text" + }, + "discord-api-token": { + "type": "text" + }, + "new-relic-user-service-id": { + "type": "text" + }, + "slack-legacy-bot-secret": { + "type": "text" + } + } + }, + "twitter-access-token": { + "type": "text" + } + } + }, + "additional_6": { + "properties": { + "adobe-client-id": { + "type": "text" + }, + "auto_apply_builders": { + "type": "boolean" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "dev_options": { + "properties": { + "droneci-access-secret": { + "type": "text" + }, + "github-pat": { + "type": "text" + }, + "github-refresh-key": { + "type": "text" + }, + "new-relic-browser-service-secret": { + "type": "text" + } + } + }, + "dropbox-long-lived-service-token": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "generate_for": { + "properties": { + "gitlab-rrt": { + "type": "text" + }, + "mailgun-signing-secret": { + "type": "text" + }, + "slack-legacy-bot-secret": { + "type": "text" + }, + "yandex-aws-access-token": { + "type": "text" + } + } + }, + "options": { + "properties": { + "digitalocean-pat": { + "type": "text" + }, + "lob-pub-service-secret": { + "type": "text" + }, + "slack-app-secret": { + "type": "text" + }, + "twitter-bearer-secret": { + "type": "text" + } + } + }, + "release_options": { + "properties": { + "hashicorp-tf-api-key": { + "type": "text" + }, + "sendbird-access-id": { + "type": "text" + }, + "shopify-private-app-access-token": { + "type": "text" + }, + "stripe-access-key": { + "type": "text" + } + } + }, + "sendbird-access-token": { + "type": "text" + }, + "square-access-key": { + "type": "text" + }, + "square-access-secret": { + "type": "text" + } + } + }, + "clojars-service-secret": { + "type": "text" + }, + "digitalocean-pat": { + "type": "text" + }, + "kucoin-access-key": { + "type": "text" + }, + "new-relic-user-service-key": { + "type": "text" + } + } + }, + "linkedin-client-key": { + "type": "text" + }, + "shopify-private-app-access-key": { + "type": "text" + }, + "slack-config-refresh-token": { + "type": "text" + } + } + }, + "additional_7": { + "properties": { + "auto_apply_builders": { + "type": "boolean" + }, + "builders": { + "properties": { + "additional_0": { + "properties": { + "doppler-service-key": { + "type": "text" + }, + "doppler-service-token": { + "type": "text" + }, + "frameio-service-token": { + "type": "text" + }, + "generate_for": { + "properties": { + "frameio-api-secret": { + "type": "text" + }, + "plaid-client-id": { + "type": "text" + }, + "private-key": { + "type": "text" + }, + "sidekiq-key": { + "type": "text" + } + } + }, + "mattermost-access-key": { + "type": "text" + } + } + }, + "alibaba-secret-token": { + "type": "text" + }, + "easypost-service-key": { + "type": "text" + }, + "netlify-access-secret": { + "type": "text" + }, + "sidekiq-sensitive-url": { + "type": "text" + } + } + }, + "digitalocean-pat": { + "type": "text" + }, + "jwt-base64": { + "type": "text" + }, + "planetscale-password": { + "type": "text" + }, + "sendinblue-service-token": { + "type": "text" + } + } + }, + "bitbucket-client-id": { + "type": "text" + }, + "digitalocean-access-key": { + "type": "text" + }, + "discord-service-token": { + "type": "text" + }, + "mailchimp-api-key": { + "type": "text" + }, + "nytimes-access-key": { + "type": "text" + }, + "plaid-secret-key": { + "type": "text" + }, + "rapidapi-access-secret": { + "type": "text" + }, + "twitch-service-key": { + "type": "text" + } + } + }, + "telegram-bot-service-key": { + "type": "text" + }, + "telegram-bot-service-secret": { + "type": "text" + }, + "tidelift": { + "type": "text" + }, + "timeout": { + "type": "long" + }, + "travisci-access-secret": { + "type": "text" + }, + "travisci-access-token": { + "type": "text" + }, + "twitch-service-token": { + "type": "text" + }, + "update_at": { + "type": "date" + }, + "url": { + "type": "keyword" + }, + "webapp": { + "properties": { + "duffel-api-key": { + "type": "text" + }, + "hubspot-api-secret": { + "type": "text" + }, + "slack-config-refresh-key": { + "type": "text" + }, + "stripe-access-secret": { + "type": "text" + } + } + } + } + }, + "instance": { + "type": "keyword" + }, + "last": { + "type": "date" + }, + "nodeinfo": { + "properties": { + "$schema": { + "type": "text" + }, + "_destroyed": { + "type": "boolean" + }, + "approval_required": { + "type": "boolean" + }, + "configuration": { + "properties": { + "accounts": { + "properties": { + "max_featured_tags": { + "type": "long" + } + } + }, + "media_attachments": { + "properties": { + "image_matrix_limit": { + "type": "long" + }, + "image_size_limit": { + "type": "long" + }, + "supported_mime_types": { + "type": "text" + }, + "video_frame_rate_limit": { + "type": "long" + }, + "video_matrix_limit": { + "type": "long" + }, + "video_size_limit": { + "type": "long" + } + } + }, + "polls": { + "properties": { + "max_characters_per_option": { + "type": "long" + }, + "max_expiration": { + "type": "long" + }, + "max_options": { + "type": "long" + }, + "min_expiration": { + "type": "long" + } + } + }, + "statuses": { + "properties": { + "characters_reserved_per_url": { + "type": "long" + }, + "max_characters": { + "type": "long" + }, + "max_media_attachments": { + "type": "long" + } + } + } + } + }, + "contact_account": { + "properties": { + "acct": { + "type": "text" + }, + "avatar": { + "type": "text" + }, + "avatar_static": { + "type": "text" + }, + "bot": { + "type": "boolean" + }, + "created_at": { + "type": "date" + }, + "discoverable": { + "type": "boolean" + }, + "display_name": { + "type": "text" + }, + "fields": { + "properties": { + "name": { + "type": "text" + }, + "value": { + "type": "text" + }, + "verified_at": { + "type": "date" + } + } + }, + "followers_count": { + "type": "long" + }, + "following_count": { + "type": "long" + }, + "group": { + "type": "boolean" + }, + "header": { + "type": "text" + }, + "header_static": { + "type": "text" + }, + "hide_collections": { + "type": "boolean" + }, + "id": { + "type": "text" + }, + "indexable": { + "type": "boolean" + }, + "last_status_at": { + "type": "date" + }, + "locked": { + "type": "boolean" + }, + "noindex": { + "type": "boolean" + }, + "note": { + "type": "text" + }, + "roles": { + "properties": { + "color": { + "type": "text" + }, + "id": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "statuses_count": { + "type": "long" + }, + "uri": { + "type": "text" + }, + "url": { + "type": "text" + }, + "username": { + "type": "text" + } + } + }, + "description": { + "type": "text" + }, + "email": { + "type": "text" + }, + "error": { + "type": "text" + }, + "invites_enabled": { + "type": "boolean" + }, + "languages": { + "type": "text" + }, + "metadata": { + "properties": { + "accountActivationRequired": { + "type": "boolean" + }, + "chat_enabled": { + "type": "boolean" + }, + "disableBubbleTimeline": { + "type": "boolean" + }, + "disableGlobalTimeline": { + "type": "boolean" + }, + "disableLocalTimeline": { + "type": "boolean" + }, + "disableRecommendedTimeline": { + "type": "boolean" + }, + "disableRegistration": { + "type": "boolean" + }, + "donationUrl": { + "type": "text" + }, + "emailRequiredForSignup": { + "type": "boolean" + }, + "enableDiscordIntegration": { + "type": "boolean" + }, + "enableEmail": { + "type": "boolean" + }, + "enableFC": { + "type": "boolean" + }, + "enableGithubIntegration": { + "type": "boolean" + }, + "enableGlobalTimeline": { + "type": "boolean" + }, + "enableGuestTimeline": { + "type": "boolean" + }, + "enableHcaptcha": { + "type": "boolean" + }, + "enableLocalTimeline": { + "type": "boolean" + }, + "enableMcaptcha": { + "type": "boolean" + }, + "enableRecaptcha": { + "type": "boolean" + }, + "enableRecommendedTimeline": { + "type": "boolean" + }, + "enableServiceWorker": { + "type": "boolean" + }, + "enableTurnstile": { + "type": "boolean" + }, + "enableTwitterIntegration": { + "type": "boolean" + }, + "explicitContent": { + "type": "boolean" + }, + "features": { + "type": "text" + }, + "federatedTimelineAvailable": { + "type": "boolean" + }, + "federation": { + "properties": { + "enabled": { + "type": "boolean" + }, + "exclusions": { + "type": "boolean" + }, + "mrf_hashtag": { + "properties": { + "sensitive": { + "type": "text" + } + } + }, + "mrf_hellthread": { + "properties": { + "delist_threshold": { + "type": "long" + }, + "reject_threshold": { + "type": "long" + } + } + }, + "mrf_keyword": { + "properties": { + "reject": { + "type": "text" + }, + "replace": { + "properties": { + "pattern": { + "type": "text" + }, + "replacement": { + "type": "text" + } + } + } + } + }, + "mrf_object_age": { + "properties": { + "actions": { + "type": "text" + }, + "threshold": { + "type": "long" + } + } + }, + "mrf_policies": { + "type": "text" + }, + "mrf_simple": { + "properties": { + "federated_timeline_removal": { + "type": "text" + }, + "followers_only": { + "type": "text" + }, + "media_removal": { + "type": "text" + }, + "reject": { + "type": "text" + }, + "reject_deletes": { + "type": "text" + } + } + }, + "mrf_simple_info": { + "properties": { + "followers_only": { + "type": "object", + "enabled": false + }, + "media_removal": { + "type": "object", + "enabled": false + }, + "reject": { + "type": "object", + "enabled": false + }, + "reject_deletes": { + "type": "object", + "enabled": false + } + } + }, + "quarantined_instances_info": { + "properties": { + "quarantined_instances": { + "type": "object", + "enabled": false + } + } + }, + "rejected_instances": { + "type": "object", + "enabled": false + } + } + }, + "feedbackUrl": { + "type": "text" + }, + "fieldsLimits": { + "properties": { + "maxFields": { + "type": "long" + }, + "maxRemoteFields": { + "type": "long" + }, + "nameLength": { + "type": "long" + }, + "valueLength": { + "type": "long" + } + } + }, + "impressumUrl": { + "type": "text" + }, + "inquiryUrl": { + "type": "text" + }, + "invitesEnabled": { + "type": "boolean" + }, + "langs": { + "type": "text" + }, + "mailerEnabled": { + "type": "boolean" + }, + "maintainer": { + "properties": { + "email": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "maxAltTextLength": { + "type": "long" + }, + "maxCaptionTextLength": { + "type": "long" + }, + "maxCwLength": { + "type": "long" + }, + "maxNoteTextLength": { + "type": "long" + }, + "maxRemoteAltTextLength": { + "type": "long" + }, + "maxRemoteCwLength": { + "type": "long" + }, + "maxRemoteNoteTextLength": { + "type": "long" + }, + "nodeAdmins": { + "properties": { + "email": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "nodeDescription": { + "type": "text" + }, + "nodeName": { + "type": "text" + }, + "pollLimits": { + "properties": { + "max_expiration": { + "type": "long" + }, + "max_option_chars": { + "type": "long" + }, + "max_options": { + "type": "long" + }, + "min_expiration": { + "type": "long" + } + } + }, + "postEditing": { + "type": "boolean" + }, + "postFormats": { + "type": "text" + }, + "postImports": { + "type": "boolean" + }, + "post_formats": { + "type": "text" + }, + "privacyPolicyUrl": { + "type": "text" + }, + "private": { + "type": "boolean" + }, + "proxyAccountName": { + "type": "text" + }, + "publicTimelineVisibility": { + "properties": { + "bubble": { + "type": "boolean" + }, + "federated": { + "type": "boolean" + }, + "local": { + "type": "boolean" + } + } + }, + "repositoryUrl": { + "type": "text" + }, + "restrictedNicknames": { + "type": "text" + }, + "roles": { + "properties": { + "admin": { + "type": "text" + }, + "moderator": { + "type": "text" + } + } + }, + "searchFilters": { + "type": "boolean" + }, + "skipThreadContainment": { + "type": "boolean" + }, + "staffAccounts": { + "type": "text" + }, + "suggestions": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "themeColor": { + "type": "text" + }, + "tosUrl": { + "type": "text" + }, + "uploadLimits": { + "properties": { + "avatar": { + "type": "long" + }, + "background": { + "type": "long" + }, + "banner": { + "type": "long" + }, + "general": { + "type": "long" + } + } + }, + "upstream": { + "properties": { + "name": { + "type": "text" + }, + "version": { + "type": "text" + } + } + } + } + }, + "mulukhiya": { + "properties": { + "config": { + "properties": { + "controller": { + "type": "text" + }, + "status": { + "properties": { + "default_hashtag": { + "type": "text" + }, + "spoiler": { + "properties": { + "emoji": { + "type": "text" + }, + "shortcode": { + "type": "text" + } + } + } + } + } + } + }, + "package": { + "properties": { + "authors": { + "type": "text" + }, + "description": { + "type": "text" + }, + "email": { + "type": "text" + }, + "license": { + "type": "text" + }, + "url": { + "type": "text" + }, + "version": { + "type": "text" + } + } + } + } + }, + "openRegistrations": { + "type": "boolean" + }, + "operations": { + "properties": { + "com": { + "properties": { + "shinolabs": { + "properties": { + "api": { + "properties": { + "bite": { + "type": "text" + } + } + } + } + } + } + }, + "jetzt": { + "properties": { + "mia": { + "properties": { + "ns": { + "properties": { + "activitypub": { + "properties": { + "accept": { + "properties": { + "bite": { + "type": "text" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "protocols": { + "type": "text" + }, + "registrations": { + "type": "boolean" + }, + "rules": { + "properties": { + "hint": { + "type": "text" + }, + "id": { + "type": "text" + }, + "text": { + "type": "text" + } + } + }, + "services": { + "properties": { + "inbound": { + "type": "text" + }, + "outbound": { + "type": "text" + } + } + }, + "short_description": { + "type": "text" + }, + "software": { + "properties": { + "codename": { + "type": "text" + }, + "edition": { + "type": "text" + }, + "homepage": { + "type": "text" + }, + "name": { + "type": "text" + }, + "repository": { + "type": "text" + }, + "version": { + "type": "text" + } + } + }, + "stats": { + "properties": { + "domain_count": { + "type": "long" + }, + "status_count": { + "type": "long" + }, + "user_count": { + "type": "long" + } + } + }, + "thumbnail": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uri": { + "type": "text" + }, + "urls": { + "properties": { + "streaming_api": { + "type": "text" + } + } + }, + "usage": { + "properties": { + "localComments": { + "type": "long" + }, + "localPosts": { + "type": "long" + }, + "users": { + "properties": { + "activeHalfYear": { + "type": "long" + }, + "activeHalfyear": { + "type": "long" + }, + "activeMonth": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + }, + "version": { + "type": "text" + } + } + }, + "peers": { + "type": "text" + } + } + }, + "settings": { + "index.mapping.total_fields.limit": 10000, + "analysis": { + "analyzer": { + "default": { + "filter": [ + "lowercase", + "asciifolding", + "stop" + ], + "tokenizer": "whitespace" + } + } + } + } +} \ No newline at end of file diff --git a/back/index.js b/back/index.js new file mode 100644 index 0000000..ea89f3a --- /dev/null +++ b/back/index.js @@ -0,0 +1,3 @@ +const server = require('./lib/express') + +console.log('Server listening on ' + server.address().address + ':' + server.address().port) \ No newline at end of file diff --git a/back/lib/apex.js b/back/lib/apex.js new file mode 100644 index 0000000..7c84889 --- /dev/null +++ b/back/lib/apex.js @@ -0,0 +1,33 @@ +const ActivitypubExpress = require('activitypub-express'), + constant = require('../constant'), + routes = { + actor: '/u/:actor', + object: '/o/:id', + activity: '/s/:id', + inbox: '/u/:actor/inbox', + outbox: '/u/:actor/outbox', + followers: '/u/:actor/followers', + following: '/u/:actor/following', + liked: '/u/:actor/liked', + collections: '/u/:actor/c/:id', + blocked: '/u/:actor/blocked', + rejections: '/u/:actor/rejections', + rejected: '/u/:actor/rejected', + shares: '/s/:id/shares', + likes: '/s/:id/likes' + }, + apex = ActivitypubExpress({ + name: 'Fediblock Instance', + version: '1.0.0', + domain: constant.apexdomain, + actorParam: 'actor', + objectParam: 'id', + activityParam: 'id', + routes, + endpoints: { + proxyUrl: 'https://' + constant.apexdomain + '/proxy' + }, + itemsPerPage: 200 + }) + +module.exports = { routes, apex } \ No newline at end of file diff --git a/back/lib/apexcustom.js b/back/lib/apexcustom.js new file mode 100644 index 0000000..fd0837f --- /dev/null +++ b/back/lib/apexcustom.js @@ -0,0 +1,97 @@ +// custom side-effects for your app +module.exports = (app, apex, client) => { + const { sendFederatedMessage, requestPart } = require('./util'), + constant = require('../constant') + app.on('apex-outbox', msg => { + if (typeof msg === 'object' + && !Object.keys(msg).filter(prop => msg[prop] + && typeof msg[prop] === 'object').some(prop => !(msg[prop].hasOwnProperty('id') + && msg[prop].hasOwnProperty('type'))) + && msg.activity + && msg.activity.type) { + console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`) + } else { + console.log(JSON.stringify(msg, '', 2)) + } + }) + app.on('apex-inbox', async msg => { + if (typeof msg === 'object' + && !Object.keys(msg).filter(prop => msg[prop] + && typeof msg[prop] === 'object').some(prop => !(msg[prop].hasOwnProperty('id') + && msg[prop].hasOwnProperty('type'))) + && msg.activity + && msg.activity.type) { + const type = msg.activity.type.toLowerCase() + if (type === 'follow' && msg.recipient.type.toLowerCase() === 'person') { + await apex.acceptFollow(msg.recipient, msg.activity) + const act = await apex.buildActivity('Accept', apex.utils.usernameToIRI(constant.nick), ['https://www.w3.org/ns/activitystreams#Public'].concat([msg.actor.id]), { + actor: msg.recipient, + object: msg.activity + }) + await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true), act) + } + if (type === 'create' && msg.object.type.toLowerCase() === 'note' && msg.actor.preferredUsername && msg.object.content) { + const name = Array.isArray(msg.actor.preferredUsername) ? msg.actor.preferredUsername[0] : msg.actor.preferredUsername, + content = ('' + msg.object.content).replace(/<[^>]*>?/gm, '').split(' ').filter(token => !token.startsWith('@')) + if (msg.recipient.id === apex.utils.usernameToIRI(constant.nick)) { + if (content.length === 1) { + try { + const instance = content.join('').trim() + if (instance === 'stats') { + const statsres = await requestPart('https://' + constant.apexdomain + '/api/stats') + await sendFederatedMessage(constant.nick, null, `STATS\n +Statuses AVG: ${Math.round(statsres.status_avg)} +Statuses MAX: ${statsres.status_max} +Domain AVG: ${Math.round(statsres.domain_avg)} +Domain MAX: ${statsres.domain_max} +Users AVG: ${Math.round(statsres.user_avg)} +Users MAX: ${statsres.user_max} +Stats Instances: ${statsres.stats_filtered} +Total Instances: ${statsres.instance_count} +Users by Instance: ${(Math.round(statsres.user_avg) / statsres.instance_count).toFixed(2)} +Statuses by Domain: ${(Math.round(statsres.status_avg) / Math.round(statsres.domain_avg)).toFixed(2)} +Statuses by User: ${(Math.round(statsres.status_avg) / Math.round(statsres.user_avg)).toFixed(2)} +https://${constant.apexdomain}`, msg.actor.id, msg.object.id) + } else { + const json = await requestPart('https://' + constant.apexdomain + '/api/detail/' + instance) + if (json && json.blocks && Array.isArray(json.blocks) && json.blocks.length > 0) { + const res = await requestPart('https://' + constant.apexdomain + '/api/list/' + instance) + if (res && res.instances && Array.isArray(res.instances)) { + res.instances.map(async r => { + if (r.domain === instance) { + await sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks\n' + + (r.api.title ? '\n' + + r.api.title + ' - ' + r.api.uri + '\n' + + (r.api.email ? 'Email: ' + r.api.email + '\n' : '') + + 'Registration: ' + (r.api.registrations ? 'open' : 'closed') + ' - Version: ' + r.api.version + '\n' + + (r.api.stats ? 'Users: ' + r.api.stats.user_count + ' - Statuses: ' + r.api.stats.status_count + ' - Domains: ' + r.api.stats.domain_count + '\n' : '') + + (r.api.description ? 'Description: ' + r.api.description + '\n' : '') : '') + + 'https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id) + } + }) + } else { + await sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks - https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id) + } + } else { + await sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' not found, next try.', msg.actor.id, msg.object.id) + } + } + } catch (e) { + await sendFederatedMessage(constant.nick, null, e.message, msg.actor.id, msg.object.id) + // console.error(e) + } + } else { + await sendFederatedMessage(constant.nick, 'Hi ' + name, 'I know ' + (await client.count({ index: constant.index })).count + ' Federated Instances\nScanning ' + app.locals.server + ' instance with ' + app.locals.scantotal + ' peers\nScanned ' + app.locals.peers + ' peers from ' + app.locals.instances + ' instances, ' + app.locals.created + ' created, ' + app.locals.updated + ' updated\nhttps://' + constant.apexdomain, msg.actor.id, msg.object.id) + } + } + console.log(`New note from ${name} to ${msg.recipient.id}: ${content.join(' ')}`) + } else if (type !== 'delete') { + console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`) + } else { + console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`) + } + } else { + console.log(JSON.stringify(msg, '', 2)) + } + }) +} diff --git a/back/lib/api.js b/back/lib/api.js new file mode 100644 index 0000000..4318f7c --- /dev/null +++ b/back/lib/api.js @@ -0,0 +1,54 @@ +module.exports = (app, apex) => { + const constant = require('../constant'), + { readFile, writeFile } = require('fs'), + asyncHandler = require('express-async-handler') + app.get('/api/scan', (req, res) => { + res.header('Content-Type', 'text/event-stream') + res.header('Connection', 'keep-alive') + res.header('Cache-Control', 'no-cache') + res.header('X-Accel-Buffering', 'no') + res.write(Buffer.from('data: ...\n\n')) + req.app.locals.scan.on('data', data => { + res.write(Buffer.from(data)) + }) + res.setTimeout(20000, () => { + res.write(Buffer.from('data: ...\n\n')) + }) + }) + app.get('/api/served', (req, res, next) => { + readFile(__dirname + '/../served.txt', async (err, data) => { + if (err) { + next(err) + } else { + const num = parseInt(data.toString()) + 1 + writeFile(__dirname + '/../served.txt', num.toString(), 'utf8', (err) => { + if (err) { + next(err) + } else { + res.json({ served: num, lastscan: req.app.locals.scantotal, server: req.app.locals.server, instances: req.app.locals.instances, peers: req.app.locals.peers, created: req.app.locals.created, updated: req.app.locals.updated }) + } + }) + } + }) + }) + app.get('/api/outbox', asyncHandler(async (req, res, next) => { + const outbox = await apex.getOutbox(await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true), 'true'), + notes = outbox.orderedItems.filter(e => e.object + && e.object.length > 0 + && e.object[0].type === 'Note' + && e.object[0].content + && e.object[0].content[0].startsWith('

You')).slice(0, 20).map(e => ({ + content: e.object[0].content[0], + published: e.published[0] + })) + res.json(notes) + })) + app.get('/api/inbox', asyncHandler(async (req, res, next) => { + const actor = await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true) + actor._local = {} + actor._local.blockList = [] + const inbox = await apex.getInbox(actor, 'true'), + notes = inbox.orderedItems.filter(e => e.object && e.object.length > 0 && e.object[0].type === 'Note').slice(0, 20).map(e => e.object[0].content[0]) + res.json(notes) + })) +} diff --git a/back/lib/apiswagger.js b/back/lib/apiswagger.js new file mode 100644 index 0000000..d67409c --- /dev/null +++ b/back/lib/apiswagger.js @@ -0,0 +1,545 @@ +module.exports = (app, client) => { + const constant = require('../constant'), + zlib = require('zlib'), + asyncHandler = require('express-async-handler'), + { param, validationResult } = require('express-validator') + /** + * @swagger + * /api/stats: + * get: + * summary: Retrieve stats of instances. + * description: Retrieve stats of total instances. + * responses: + * 200: + * description: A object with stats parameters. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * instance_count: + * type: integer + * description: Count of instances. + * example: 0 + * status_avg: + * type: float + * description: Average statuses of total instances. + * example: 0 + * status_max: + * type: integer + * description: Max of total statuses instances. + * example: 0 + * user_avg: + * type: float + * description: Average users of total instances. + * example: 0 + * user_max: + * type: integer + * description: Max of total users instances. + * example: 0 + * domain_avg: + * type: float + * description: Number of average domains instances. + * example: 0 + * domain_max: + * type: integer + * description: Number of max domains instances. + * example: 0 + * stats_filtered: + * type: integer + * description: Number of instances stats. + * example: 0 + */ + app.get('/api/stats', asyncHandler(async (req, res) => { + const result = await client.search({ + index: constant.index, + body: { + size: 0, + aggs: { + instance_count: { + value_count: { + field: '_id' + } + }, + stats_filtered: { + filter: { + exists: { + field: 'api.stats' + } + } + }, + user_avg: { + avg: { + field: 'api.stats.user_count' + } + }, + user_max: { + max: { + field: 'api.stats.user_count' + } + }, + domain_avg: { + avg: { + field: 'api.stats.domain_count' + } + }, + domain_max: { + max: { + field: 'api.stats.domain_count' + } + }, + status_avg: { + avg: { + field: 'api.stats.status_count' + } + }, + status_max: { + max: { + field: 'api.stats.status_count' + } + } + + } + } + }) + res.json(Object.keys(result.aggregations).reduce((prev, curr) => ({ + ...prev, [curr]: result.aggregations[curr][Object.keys(result.aggregations[curr])[0]] + }), {})) + })) + /** + * @swagger + * /api/count: + * get: + * summary: Retrieve count of instances. + * description: Retrieve a number of total instances. + * responses: + * 200: + * description: A object with count parameter. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * count: + * type: integer + * description: Number of instances. + * example: 0 + */ + app.get('/api/count', asyncHandler(async (req, res) => { + res.json({ count: (await client.count({ index: constant.index })).count }) + })) + /** + * @swagger + * /api/ranking: + * get: + * summary: Retrieve one ranking of instances. + * description: Retrieve a top ten ranking of total fediblock instances. + * responses: + * 200: + * description: A object with count parameter. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * domain: + * type: string + * description: Domain of the instance. + * example: "mastodon.social" + * count: + * type: integer + * description: Number of fediblocks. + * example: 0 + */ + app.get('/api/ranking', asyncHandler(async (req, res) => { + const result = await client.search({ + index: constant.index, + body: { + size: 0, + aggs: { + blocks: { + nested: { + path: 'blocks' + }, + aggs: { + ranking: { + terms: { + field: 'blocks.domain', + size: 100 + } + } + } + } + } + } + }) + const ranking = result.aggregations?.blocks?.ranking?.buckets + res.json(ranking.map(r => ({ domain: r.key, count: r.doc_count }))) + })) + /** + * @swagger + * /api/list/{instance}: + * get: + * summary: Search result array of intances. + * description: Retrieve a result array of instances matching search input. + * parameters: + * - in: path + * name: instance + * required: true + * description: String for search instance + * schema: + * type: string + * responses: + * 200: + * description: A list of instances. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * instances: + * type: array + * items: + * type: data + * description: List of instances. + * example: "mastodon.social" + * suggests: + * type: array + * items: + * type: data + * description: Suggest of the instance. + * example: "mastodon.social" + */ + app.get('/api/list/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array() + }) + } + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 10, + query: { + wildcard: { + instance: { + value: `*${req.params.instance}*` + } + }, + }, + suggest: { + suggests: { + text: req.params.instance, + term: { + field: 'instance', + size: 3, + sort: 'score', + suggest_mode: 'always', + max_edits: 2, + min_word_length: 1 + } + } + } + }) + const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [], + suggests = result.suggest.suggests && result.suggest.suggests.length > 0 && result.suggest.suggests[0].options.length > 0 ? result.suggest.suggests[0].options : [] + res.json({ + instances: instances.map(instance => ({ + domain: instance._source.instance, + api: instance._source.api ? instance._source.api : null, + blocks: instance._source.blocks && instance._source.blocks.length > 0 ? instance._source.blocks.length : null, + last: instance._source.last ? instance._source.last : null, + nodeinfo: instance._source.nodeinfo ? true : false + })), suggests: suggests.map(instance => instance.text) + }) + } else { + res.status(404).end() + } + })) + /** + * @swagger + * /api/detail/{instance}: + * get: + * summary: Search result array of fediblocks intances. + * description: Retrieve a result array of fediblock instances matching search input. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to detail of the instance + * schema: + * type: string + * responses: + * 200: + * description: A list of instances. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * domain: + * type: string + * description: Domain of the block instance. + * example: "mastodon.social" + * comment: + * type: string + * description: Comment of the block instance. + * example: "mastodon.social" + * severity: + * type: string + * description: Severity of the block instance. + * example: "mastodon.social" + * + */ + app.get('/api/detail/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array() + }) + } + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: req.params.instance + } + } + }) + const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [] + res.json(instances.length > 0 && instances[0]._source.blocks && instances[0]._source.blocks.length > 0 ? + { + blocks: instances[0]._source.blocks.map(instance => ({ domain: instance.domain, comment: instance.comment, severity: instance.severity })), + last: instances[0]._source.last, + instance: instances[0]._source.instance, + nodeinfo: instances[0]._source.nodeinfo, + api: instances[0]._source.api, + took: result.took + } : []) + } else { + res.status(404).end() + } + })) + /** + * @swagger + * /api/detail_api/{instance}: + * get: + * summary: Search result of detail's api intance. + * description: Retrieve a result of detail's api instance. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to detail of the instance + * schema: + * type: string + * responses: + * 200: + * description: Detail of the api instance. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + */ + app.get('/api/detail_api/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array() + }) + } + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: req.params.instance + } + } + }) + const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [] + res.json(instances.length > 0 ? instances[0]._source.api : {}) + } else { + res.status(404).end() + } + })) + /** + * @swagger + * /api/detail_nodeinfo/{instance}: + * get: + * summary: Search result of detail's nodeinfo intance. + * description: Retrieve a result of detail's nodeinfo instance. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to detail of the instance + * schema: + * type: string + * responses: + * 200: + * description: Detail of the nodeinfo instance. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + */ + app.get('/api/detail_nodeinfo/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array() + }) + } + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: req.params.instance + } + } + }) + const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [] + res.json(instances.length > 0 ? instances[0]._source.nodeinfo : {}) + } else { + res.status(404).end() + } + })) + /** + * @swagger + * /api/block_count/{instance}: + * get: + * summary: Retrieve count of fediblocked instances. + * description: Retrieve a number of total fediblocked instances. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to search fediblocks of the instance + * schema: + * type: string + * responses: + * 200: + * description: A object with block_count parameter. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * block_count: + * type: integer + * description: Number of fediblock instances. + * example: 0 + * instances: + * type: array + * items: + * type: object + * properties: + * instance: + * type: string + * description: Instance of the block. + * example: mastodon.social + * comment: + * type: string + * description: Comment about the block. + * example: spam + */ + app.get('/api/block_count/:instance', param('instance').notEmpty().trim().escape(), asyncHandler(async (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array() + }) + } + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 9999, + query: { + nested: { + path: 'blocks', + query: { + term: { + 'blocks.domain': req.params.instance + } + } + } + } + }) + const instances = result.hits?.hits?.length > 0 ? result.hits.hits : [], + instancescomment = instances.map(i => ({ + instance: i._source.instance, comment: i._source.blocks.find(block => block.domain === req.params.instance).comment + })) + res.json({ + block_count: instances.length, + instances: instancescomment, + took: result.took + }) + } else { + res.status(404).end() + } + })) + /** + * @swagger + * /api/download_index: + * get: + * summary: Retrieve all content of ElasticSearch index. + * description: Retrieve all content of ElasticSearch index. + * responses: + * 200: + * description: A file compressed with gzip. + * content: + * application/gzip: + */ + app.get('/api/download_index', asyncHandler(async (req, res) => { + try { + res.setHeader('Content-Type', 'application/gzip') + res.setHeader('Content-disposition', 'attachment; filename=fediblock-index.json.gz') + const result = await client.search({ + index: constant.index, + size: 9999, + query: { + match_all: {} + } + }, { asStream: true, meta: false }) + result.pipe(zlib.createGzip()).pipe(res) + } catch (e) { + console.error(e) + res.status(404).end() + } + })) +} diff --git a/back/lib/express.js b/back/lib/express.js new file mode 100644 index 0000000..28262b8 --- /dev/null +++ b/back/lib/express.js @@ -0,0 +1,158 @@ +const http = require('http'), + express = require('express'), + rateLimit = require('express-rate-limit'), + app = express(), + events = require('events'), + constant = require('../constant'), + logger = require('./logger'), + { routes, apex } = require('./apex'), + apexcustom = require('./apexcustom'), + apiswagger = require('./apiswagger'), + api = require('./api'), + swagger = require('./swagger'), + fediblock = require('./fediblock'), + taskdeletedup = require('./taskdeletedup'), + { generateKeyPairSync } = require('crypto'), + { MongoClient } = require('mongodb'), + mongoclient = new MongoClient(constant.dburl), + { Client } = require('@elastic/elasticsearch'), + client = new Client({ + node: constant.elasticnode, + pingTimeout: 10000, + requestTimeout: 60000, + retryOnTimeout: true, + maxRetries: 3 + }), + register = async () => { + const admin = await apex.store.getObject(apex.utils.usernameToIRI('admin'), true) + if (!admin) { + const adminaccount = await apex.createActor('admin', 'Fediblock Admin', 'Fediblock Admin - https://' + constant.apexdomain, + { type: 'Image', mediaType: 'image/png', url: constant.icon }, 'Service'), + keys = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) + adminaccount.publicKey[0].publicKeyPem[0] = keys.publicKey + adminaccount._meta.privateKey = keys.privateKey + await apex.store.saveObject(adminaccount) + const bot = await apex.createActor(constant.nick, 'Fediblock Bot', 'Fediblock #Bot - https://' + constant.apexdomain, + { type: 'Image', mediaType: 'image/png', url: constant.icon }, 'Person'), + keysbot = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) + bot.publicKey[0].publicKeyPem[0] = keysbot.publicKey + bot._meta.privateKey = keysbot.privateKey + await apex.store.saveObject(bot) + apex.systemUser = adminaccount + apex.store.setup(adminaccount) + } else { + apex.systemUser = admin + } + }, + connect = () => { + mongoclient.connect() + .then(async () => { + const exists = await client.indices.exists({ index: constant.index }) + if (!exists) { + await client.indices.create({ + index: constant.index, + body: require('../fediblock-mapping.json') + }) + } + apex.store.db = mongoclient.db(constant.dbname) + return apex.store.db + }) + .then(async () => { + try { + await register() + taskdeletedup(client) + await apex.startDelivery() + await fediblock(client, app) + } catch (e) { + console.error(e) + } + }) + }, + server = http.createServer(app).listen(4000, () => { + connect() + }) + +app.locals.scan = new events.EventEmitter() +app.locals.scannum = 0 +app.locals.scantotal = 0 +app.locals.server = '' +app.locals.peers = 0 +app.locals.instances = 0 +app.locals.created = 0 +app.locals.updated = 0 +app.disable('x-powered-by') +app.set('json spaces', 2) +app.set('trust proxy', 1) +app.use(logger) +app.use(rateLimit({ + windowMs: 1 * 60 * 1000, // 1 minutes + limit: 120, // each IP can make up to 120 requests per `windowsMs` (5 minutes) + standardHeaders: true, // add the `RateLimit-*` headers to the response + legacyHeaders: false, + delayAfter: 30, // allow 30 requests per `windowMs` (5 minutes) without slowing them down + delayMs: (hits) => hits * 200, // add 200 ms of delay to every request after the 10th + maxDelayMs: 5000 +})) +app.use( + express.json({ type: apex.consts.jsonldTypes }), + express.urlencoded({ extended: true }), + apex +) +app.route(routes.inbox) + .get(apex.net.inbox.get) + .post(apex.net.inbox.post) +app.route(routes.outbox) + .get(apex.net.outbox.get) + .post(apex.net.outbox.post) +app.get(routes.actor, apex.net.actor.get) +app.get(routes.followers, apex.net.followers.get) +app.get(routes.following, apex.net.following.get) +app.get(routes.liked, apex.net.liked.get) +app.get(routes.object, apex.net.object.get) +app.get(routes.activity, apex.net.activityStream.get) +app.get(routes.shares, apex.net.shares.get) +app.get(routes.likes, apex.net.likes.get) +app.get('/.well-known/webfinger', apex.net.webfinger.get) +app.get('/.well-known/nodeinfo', apex.net.nodeInfoLocation.get) +app.get('/nodeinfo/:version', apex.net.nodeInfo.get) +app.post('/proxy', apex.net.proxy.post) +apexcustom(app, apex, client) +api(app, apex, client) +apiswagger(app, client) +swagger(app) +app.get('/u/' + constant.nick, (req, res) => { + res.redirect('/api/inbox') +}) +app.get(Object.keys(routes).map(route => routes[route]), (req, res) => { + res.redirect('/') +}) +app.use(express.static(__dirname + '/../../front/build')) + .use((err, req, res, next) => { + if (res.headersSent) { + console.error(err.stack) + return next(err) + } + res.status(500).end('Error') + }) + +module.exports = server \ No newline at end of file diff --git a/back/lib/fediblock.js b/back/lib/fediblock.js new file mode 100644 index 0000000..fd5f2ae --- /dev/null +++ b/back/lib/fediblock.js @@ -0,0 +1,211 @@ +module.exports = async (client, app) => { + const { sendFederatedMessage, requestPart } = require('./util'), + constant = require('../constant'), + schedule = require('node-schedule'), + { PromisePool } = require('@supercharge/promise-pool'), + workers = constant.workers, + threads = constant.threads, + getAccount = api => { + if (api && api.contact_account.acct) { + const acct = api.contact_account.acct.split('@') + if (acct.length > 1) { + return `https://${acct[1]}/users/${acct[0]}` + } else { + return `https://${api.uri.replace(/https?:\/\//, '')}/users/${acct.join('')}` + } + } else { + return '' + } + }, + scanInstance = async instance => { + const json = await requestPart(`https://${instance}/api/v1/instance/domain_blocks`) + if (Array.isArray(json) && json.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: instance + } + } + }), + instancelocated = result.hits && result.hits.hits ? result.hits.hits : [], + blocks = json.map(block => { + if (block.comment && block.comment.length > 8190) { + block.comment = block.comment.slice(0, 8190) + } + return block + }), + [api, nodeinfo, peers] = await Promise.all([ + requestPart(`https://${instance}/api/v1/instance`), + requestPart(`https://${instance}/nodeinfo/2.0`), + requestPart(`https://${instance}/api/v1/instance/peers`) + ]) + if (result && result.hits) { + if (instancelocated.length === 0) { + await client.index({ + index: constant.index, + body: { + instance, + api, + nodeinfo, + blocks, + peers, + last: new Date() + } + }) + app.locals.created++ + return sendFederatedMessage(constant.nick, 'New Fediblock Instance', `Fediblock Instance ${instance} with ${json.length} blocks - https://${constant.apexdomain}#${instance}`, getAccount(api)) + } else { + const elasticinstance = instancelocated[0]._source.blocks || [] + if (Array.isArray(elasticinstance)) { + if (json.length !== elasticinstance.length + || (instancelocated[0]._source.last && instancelocated[0]._source.last < new Date(Date.now() - 2678400000)) + || !instancelocated[0]._source.api + || !instancelocated[0]._source.nodeinfo + || !instancelocated[0]._source.peers) { + await client.update({ + index: constant.index, + id: instancelocated[0]._id, + doc: { + api: api ? api : instancelocated[0]._source.api, + nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : elasticinstance, + peers: peers && peers.length > 0 ? peers : instancelocated[0]._source.peers, + last: new Date() + }, + doc_as_upsert: true + }) + app.locals.updated++ + if (instancelocated[0]._source.api && instancelocated[0]._source.api.uri && instancelocated[0]._source.api.contact_account && instancelocated[0]._source.api.contact_account.acct) { + const difference = blocks.filter(block => block.domain && block.domain.trim().length > 0 && !elasticinstance.some(instance => block.domain === instance.domain)) + if (difference.length > 0 && difference.length < 20) { + return sendFederatedMessage(constant.nick, 'Detected #Fediblock by Fediblock Instance', `You blocked new instances: ${difference.map(d => d.domain).join(', ')} - https://${constant.apexdomain}#${instance}`, getAccount(instancelocated[0]._source.api)) + } + } + } + } + } + } + } + }, + scanPart = async instancesall => PromisePool + .withConcurrency(threads) + .for(instancesall) + .process(async instance => { + app.locals.scan.emit('data', 'data: ' + (app.locals.scannum > 0 ? '(' + app.locals.scannum-- + '): ' + instance : ': ' + instance) + '\n\n') + app.locals.peers++ + return scanInstance(instance) + }), + scanIndex = async (server, instancesall) => { + const [api, nodeinfo, blocks] = await Promise.all([ + requestPart(`https://${server}/api/v1/instance`), + requestPart(`https://${server}/nodeinfo/2.0`), + requestPart(`https://${server}/api/v1/instance/domain_blocks`) + ]) + if (api && typeof api === 'object' && api.version) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: server + } + } + }), + instancelocated = result.hits && result.hits.hits ? result.hits.hits : [] + if (result && result.hits) { + if (instancelocated.length === 0) { + await client.index({ + index: constant.index, + body: { + instance: server, + peers: instancesall, + api, + nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : [], + last: new Date() + } + }) + app.locals.created++ + } else { + const elasticinstance = instancelocated[0]._source.peers || [] + if (Array.isArray(elasticinstance) && Array.isArray(instancesall) && instancesall.length > 0) { + if (instancesall.length !== elasticinstance.filter(i => !constant.filterdomains.some(d => i.endsWith(d))).length) { + await client.update({ + index: constant.index, + id: instancelocated[0]._id, + doc: { + peers: instancesall.length > 0 ? instancesall : elasticinstance, + api: api ? api : instancelocated[0]._source.api, + nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : instancelocated[0]._source.blocks, + last: new Date() + }, + doc_as_upsert: true + }) + app.locals.updated++ + } + } + } + } + } + }, + scanReturn = async () => { + app.locals.scannum = 0 + if (app.locals.servers?.length > 0) { + await scan(app.locals.servers.shift()) + } else { + await scan(constant.initialscan) + } + }, + scan = async server => { + if (server) { + try { + console.log(server) + app.locals.scan.emit('data', 'data: ' + server + ' peers...\n\n') + const instances = await requestPart(`https://${server}/api/v1/instance/peers`), + instancesall = Array.isArray(instances) && instances.length > 0 + ? instances.filter(i => !constant.filterdomains.some(d => i.endsWith(d))) + : [], + instancessorted = instancesall.sort((a, b) => a < b ? -1 : a > b ? 1 : 0), + parts = [], + split = workers, + chunkSize = Math.ceil(instancessorted.length / split) + if (instancesall.length > 0) { + await scanIndex(server, instancesall) + if (!app.locals.servers || app.locals.servers.length === 0) { + app.locals.servers = instancesall.sort(() => Math.random() - 0.5) + } + if (server === constant.initialscan) { + return scan(app.locals.servers.shift()) + } + app.locals.scannum = instancessorted.length + app.locals.scantotal = instancessorted.length + app.locals.server = server + app.locals.instances++ + for (let i = 0; i < instancessorted.length; i += chunkSize) { + const chunk = instancessorted.slice(i, i + chunkSize) + parts.push(chunk) + } + await PromisePool + .withConcurrency(workers) + .for(parts) + .process(scanPart) + await scanReturn() + } else { + await scanReturn() + } + } catch (e) { + // console.error(e) + await scanReturn() + } + } else { + await scanReturn() + } + }, + job = schedule.scheduleJob('0 ' + constant.schedule + ' * * *', async () => { + await sendFederatedMessage(constant.nick, null, 'Scanning ' + app.locals.server + ' instance with ' + app.locals.scantotal + ' peers\nScanned ' + app.locals.peers + ' peers from ' + app.locals.instances + ' instances, ' + app.locals.created + ' created, ' + app.locals.updated + ' updated\nhttps://' + constant.apexdomain) + }) + return scanReturn() +} diff --git a/back/lib/logger.js b/back/lib/logger.js new file mode 100644 index 0000000..b695f25 --- /dev/null +++ b/back/lib/logger.js @@ -0,0 +1,6 @@ +const morgan = require('morgan'), + rfs = require('rotating-file-stream'), + accessLogStream = rfs.createStream('access.log', { interval: '1d', path: __dirname + '/../logs' }), + logger = morgan('combined', { stream: accessLogStream }) + +module.exports = logger diff --git a/back/lib/swagger.js b/back/lib/swagger.js new file mode 100644 index 0000000..85218a3 --- /dev/null +++ b/back/lib/swagger.js @@ -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 })) +} diff --git a/back/lib/taskdeletedup.js b/back/lib/taskdeletedup.js new file mode 100644 index 0000000..a2a4092 --- /dev/null +++ b/back/lib/taskdeletedup.js @@ -0,0 +1,69 @@ +module.exports = client => { + const constant = require('../constant'), + schedule = require('node-schedule'), + { pick } = require('stream-json/filters/Pick'), + { parser } = require('stream-json'), + { streamArray } = require('stream-json/streamers/StreamArray'), + { chain } = require('stream-chain'), + size = 500, + deleteDup = async () => { + const count = { deleted: 0, total: 0, current: 0 } + let lastsort = undefined, last = undefined + await searchDup(count, lastsort, last) + }, + searchDup = async (count, lastsort, last) => { + count.current = 0 + const result = await client.search({ + index: constant.index, + size: size, + body: { + query: { + match_all: {} + }, + sort: [{ + "instance": { + "order": "asc" + }, + "last": { + "order": "desc", + "numeric_type": "date_nanos", + "format": "strict_date_optional_time_nanos" + } + }], + search_after: lastsort + } + }, { asStream: true, meta: false }), + pipeline = chain([ + parser(), + pick({ filter: 'hits.hits' }), + streamArray(), + data => data.value + ]) + pipeline.on('data', async data => { + count.current++ + if (last && last.instance === data._source.instance) { + await client.delete({ index: constant.index, id: data._id }) + count.deleted++ + console.log('deleted ' + data._id + ': ' + data._source.instance) + } + else { + last = data._source + } + if (count.current === size) { + lastsort = data.sort + } + }) + pipeline.on('end', async () => { + count.total += count.current + if (count.current === size) { + await searchDup(count, lastsort, last) + } else { + console.log('Index: ' + constant.index + ' - Total: ' + count.total + ' - Deleted: ' + count.deleted) + } + }) + result.pipe(pipeline) + }, + job = schedule.scheduleJob('0 ' + constant.taskdeletedup + ' * * *', async () => { + await deleteDup() + }) +} diff --git a/back/lib/util.js b/back/lib/util.js new file mode 100644 index 0000000..8d535ce --- /dev/null +++ b/back/lib/util.js @@ -0,0 +1,83 @@ +const constant = require("../constant"), + { apex } = require('./apex'), + https = require('node:https'), + json = 'application/json', + urlToId = url => { + try { + if (typeof new URL(url) === 'object' && url.split('/').length === 5 && url.split('/')[3].length > 0) { + return '@' + url.split('/')[4] + '@' + url.split('/')[2] + } else { + return url + } + } catch (e) { + return url + } + }, + sendFederatedMessage = async (id, summary, message, recipient, reply) => { + const users = message.split(' ').filter(token => token.startsWith('@') && token.split('@').length === 3) + .map(token => `https://${token.split('@')[2]}/users/${token.split('@')[1]}`), + mentions = message.split(' ').filter(token => token.startsWith('@') && token.split('@').length === 3) + .map(token => { return { type: 'Mention', href: `https://${token.split('@')[2]}/users/${token.split('@')[1]}`, name: `@${token.split('@')[1]}` } }), + hashtags = message.split(' ').filter(token => token.startsWith('#')) + .map(token => { return { id: `https://${constant.apexdomain}/tags/${token.split('#')[1]}`, name: token } }), + images = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(jpg|png|gif|jpeg|ppm)$/i)) + .map(token => { return { type: 'Image', url: token } }), + audio = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(mp3|wav|flac)$/i)) + .map(token => { return { type: 'Audio', url: token } }), + video = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(mp4|flv|avi)$/i)) + .map(token => { return { type: 'Video', url: token } }), + links = message.split(' ').filter(token => token.match(/^https?:\/\//i) && !images.concat(audio).concat(video) + .some(link => link.url === token)).map(token => { return { type: 'Link', href: token } }), + allusers = users.concat([recipient || (await apex.store.getObject(apex.utils.usernameToIRI(id), true)).followers[0]]), + act = await apex.buildActivity('Create', apex.utils.usernameToIRI(id), ['https://www.w3.org/ns/activitystreams#Public'].concat(allusers), { + object: { + type: 'Note', + name: `Fediblock Instance`, + summary: summary || null, + content: `

${message.replace(/(\b(https?|ftp|file):\/\/([-A-Z0-9+&@#%?=~_|!:,.;]*)([-A-Z0-9+&@#%?/=~_|!:,.;]*))/ig, + '$3').replace(/\s#(\S+)/g, '#$1').replace(/\r?\n/g, '
')}

`, + tag: hashtags.concat(mentions), + attachment: images.concat(links).concat(audio).concat(video), + inReplyTo: reply || null + } + }) + act.object[0].id = act.id + await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(id), true), act) + }, + requestPart = uri => new Promise((resolve, reject) => { + try { + const data = [], + req = https.request(uri, { + headers: { + 'User-Agent': constant.agent, + 'Content-Type': json + }, + signal: AbortSignal.timeout(constant.abort_timeout), + }, res => { + res.setEncoding('utf8') + res.headers['Content-Type'] = json + res.on('data', chunk => { + data.push(chunk) + }) + res.on('error', error => { + reject(error) + }) + res.on('end', () => { + try { + resolve(JSON.parse(data.join(''))) + } catch (error) { + reject(error) + } + }) + }) + req.on('error', error => { + reject(error) + }) + req.end() + } catch (error) { + reject(error) + } + }) + +module.exports = { urlToId, sendFederatedMessage, requestPart } + diff --git a/back/package.json b/back/package.json new file mode 100644 index 0000000..ffce9c1 --- /dev/null +++ b/back/package.json @@ -0,0 +1,35 @@ +{ + "name": "fediblock-instance", + "version": "1.0.0", + "description": "Fediblock Instance", + "author": "ale", + "repository": { + "type": "git", + "url": "https://git.manalejandro.com/ale/fediblock-instance" + }, + "license": "MIT", + "scripts": { + "start": "node index.js", + "build": "cd ../front && yarn build", + "install": "cd node_modules && rm -rf http-signature && rm -rf request/node_modules/http-signature && mv @peertube/http-signature . && cd ../../front && yarn" + }, + "private": true, + "dependencies": { + "@elastic/elasticsearch": "^8.17.1", + "@peertube/http-signature": "^1.7.0", + "@supercharge/promise-pool": "^3.2.0", + "@types/morgan": "^1.9.9", + "activitypub-express": "^4.4.2", + "express": "^4.21.2", + "express-async-handler": "^1.2.0", + "express-rate-limit": "^7.5.0", + "express-validator": "^7.2.1", + "mongodb": "^4.17.2", + "morgan": "^1.10.0", + "node-schedule": "^2.1.1", + "rotating-file-stream": "^3.2.6", + "stream-json": "^1.9.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + } +} diff --git a/back/served.txt b/back/served.txt new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/back/served.txt @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..11293b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +services: + fediblock-instance: + image: fediblock-instance + build: . + hostname: fediblock-instance + container_name: fediblock-instance + restart: always + user: node + ports: + - "4000:4000" + depends_on: + - fediblock-mongodb + - fediblock-elasticsearch + networks: + fediblocknet: + + fediblock-mongodb: + image: mongo:4.4.28 + hostname: fediblock-mongodb + container_name: fediblock-mongodb + restart: always + command: --wiredTigerCacheSizeGB 0.5 + volumes: + - ./mongodb:/data/db + networks: + fediblocknet: + + fediblock-elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.17.3-amd64 + hostname: fediblock-elasticsearch + container_name: fediblock-elasticsearch + restart: always + environment: + - node.name=fediblock-elasticsearch + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + - xpack.security.enabled=false + - indices.id_field_data.enabled=true + - discovery.type=single-node + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65535 + hard: 65535 + volumes: + - ./elasticsearch-data:/usr/share/elasticsearch/data + expose: + - 9200 + networks: + fediblocknet: + +networks: + fediblocknet: diff --git a/front/.gitignore b/front/.gitignore new file mode 100644 index 0000000..f1f2610 --- /dev/null +++ b/front/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# production +build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/front/README.md b/front/README.md new file mode 100644 index 0000000..58beeac --- /dev/null +++ b/front/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can't go back!** + +If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. + +You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `npm run build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/front/package.json b/front/package.json new file mode 100644 index 0000000..ea4fbcc --- /dev/null +++ b/front/package.json @@ -0,0 +1,38 @@ +{ + "name": "front", + "version": "0.1.0", + "private": true, + "dependencies": { + "cra-template": "1.2.0", + "dayjs": "^1.11.13", + "html2canvas": "^1.4.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-scripts": "5.0.1", + "web-vitals": "^4.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/front/public/favicon.ico b/front/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d14c02ddf08636310993d705378ac9b63c40a235 GIT binary patch literal 31014 zcmeI5O=u)V6vt~4H7hJe5yg+ygk+|AvfVw|>7Ha+M2zSmD6WV`{J1EH;0LlSh@gi= z1wr(%2NChKARZJ2(e>c>DtHk0u!x`sVFyH47s1h$7|fdSUz4t+cQW-lT^Tdo!y9P2 zrm9}Q|NC`Kef3NtL_rLQ{r3o*N5zE)g^)ss+1UZSFNnr@LQEsCjJ*9gj^I%B_{9X6 z025#WOn?b60Vco%m;e)C0!)AjFaajO1oB28^yT5ulg%z2$MP1xtzPD4M~=BMH#-7$ zR@~m0n;ii=E8q7o+FL2^YCkZ({z#3n)01V{Zd0j@UuvhKf2_c)PH}TxcW3`Z+r3Db zn;lE$mWo?J^;x0oTn}!isLyH5cW#~X<=EVu7)<4%Pbycd#Y;lZc_?({JK*#i#mZ22 z8u=fFzVpPMJ>}6p!D0*}@XKM~Id386Wdje(HEZd!z;$Mgj&oWtT`pf-^W51e^CaQAcxA=kLNWJU1`wYQDTFA$}cO zL(L`BdR4^+o5Xe2%;@M2%wHZrU2-j}{Rdv@@w{+DTwiG2#QFH-q`Wd76Y3^%)AiL_ zHJa1tdtf@B%tM>2RVF-wm5%LH)p4%S4D>k-H)W%Ez~-wYf8cwk_L0?d%_}{X6i3Z< z?&zdHEjM+Iz#T8b?hNVEbzbk}FH3nCLq8#7z-JM92RdoC;ij%r^PG1ReE!nOUzYNq zU#%VW=W*lNcb+xrwdUrQZYO<0PjzBq#N@Zv`K>t{#SCR?tTpMhJwUzNzMB5yGH z?R7r-t!t9>Q~TPs+*I7K{gv_^`0~MC^D+4k+~O^ic9bofY*jwWH$4w&zYc7VlfE%e zY`4kE`IMo}qmAvMlgR_tVD!YpQoegx z7WmG+3EEFm`nJLiZT;f}-_KFLds&9H`E3c>7b$&P;^xV(Dc@>7PWkR-8J?NkNNHDP z`}XZR-=uLX++h0y?Ys1t$#1XoqczXQxo&(;x4X30q|=t0TI0j^Uy2*%Na|;$nS9O6 z%$x)5%cOo^K57yVJF9s}P?6VCK2=%OE$%k?%|bpjenZ8e`frm?*jaJIGsPDupD0^I zo5U}gv-`p`kwXbFG;0;d=@fwtw_>q41iOT1u+9ga{ADRmJ;zMqruM|4dBdKF4L8gg zpHKAXItQ}E$e?8a`@JU;V?n&__G!7PeLUE#B>4JUJ-==DmBshigFBcIOKZ%<+URZd z-4GqaX1TTY$XDALIfk}0`0`bK{j8r=9}LakZ-yP}{Db3-Xsz)xWm9uYcVo}{71U`^ z^+YK6=gbq%X_m7+(({ zwxqth%k8c7GMH=_ud~dlleTC-lkB@p?IvaZY1PI%X~=7ydL|#u>*?n^ICW8>b$lm8{J_zHmsg+f#H*?y@2CltPOSQ z^WMbg@M-lOttr>N%*JOw+JKqWy1pM-q=|{SrJ=Q6m$|L8NRwi2G%fyJ8_ma58jYQu z#!H*LVNfC(@GCcp%k025#WOn?b6 z0Vco%m;e*#K7powoe@GOo7qn<2{GS{bapGH1D_-D-ZLP`_X;Jy;)(rx$dcq5AN`$ZGw1 zTl%QZAFdBur*X|S+qUa#zl~UaBd)U$(;SOsN!+(%Jzg-Gk1=VkrsLe2sG=D*7h;@` z3W1x~;@NBn;EB>Js(O^()cuX!_xSzoRvKc~_2fCN{vV|J!-0ozsQ(jbdLc$r5$7`j OCcp%k029bOf&T!nsD&&5 literal 0 HcmV?d00001 diff --git a/front/public/index.html b/front/public/index.html new file mode 100644 index 0000000..05667d4 --- /dev/null +++ b/front/public/index.html @@ -0,0 +1,21 @@ + + + + + Fediblock Instance Φ + + + + + + + + + + + + + +
+ + diff --git a/front/public/logo192.png b/front/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/front/public/manifest.json b/front/public/manifest.json new file mode 100644 index 0000000..fb805b4 --- /dev/null +++ b/front/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "New Fediblock Instance", + "name": "New Fediblock Instance Front", + "icons": [ + { + "src": "favicon.ico", + "sizes": "78x90 72x72", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/front/public/robots.txt b/front/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/front/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/front/src/App.css b/front/src/App.css new file mode 100644 index 0000000..6ce1625 --- /dev/null +++ b/front/src/App.css @@ -0,0 +1,405 @@ +:root[data-theme="dark"] { + --background-color: #212529; + --color: #fefefe; + --instance-list: #333333; + --footer: #cccccc; +} + +:root[data-theme="light"] { + --background-color: #f5f5f5; + --color: #333333; + --instance-list: #f0f0f0; + --footer: #666666; +} + +body { + background-color: var(--background-color); + text-align: center; + font-family: Verdana, Geneva, Tahoma, sans-serif; + scrollbar-width: thin; +} + +h1, +h3, +h4, +.scan { + margin: 0 auto; + color: var(--color); + text-align: center; +} + +h3 { + padding: 10px 0; + border-bottom: 1px solid var(--footer); + margin-bottom: 1rem; +} + +a, +a:hover { + color: var(--color); + text-decoration: none; +} + +input[type="text"] { + padding: 10px; + border: none; + border-bottom: 1px solid var(--footer); + background-color: var(--instance-list); + color: var(--color); +} + +.placeholder { + font-size: small; + margin: 0 auto; + color: var(--color); +} + +.title, .placeholder a, footer a, .download-csv { + cursor: pointer; +} + +hr { + margin: 1rem auto; + border-bottom: 1px solid var(--footer); + width: 50%; +} + +.tooltip { + visibility: hidden; + font-size: small; + text-align: left; + width: 2.2in; + background-color: var(--background-color); + color: var(--color); + border-radius: 5px; + padding: 1rem 1rem 0 0; + position: absolute; + z-index: 1; + opacity: 0; + transition: 0.5s; + border: 1px solid var(--footer); +} + +.count:hover .tooltip { + visibility: visible; + opacity: 1; +} + +.reverse { + margin: 0.5rem; + padding: 0.5rem; + border: 1px solid var(--footer); + background-color: var(--color); + color: var(--background-color); +} + +.reverse:hover { + color: var(--color); + background-color: var(--background-color); + cursor: pointer; +} + +.instancelist { + list-style-type: none; + margin: 20px 0; + padding: 0; + color: var(--color); + max-height: 58vh; + overflow: auto; + scrollbar-width: thin; +} + +@media (min-width: 992px) { + .instancelist li { + width: 30%; + } + + .instancelist li:hover { + width: 35%; + } + + .instance, + .placeholder, + h4 { + width: 30%; + } + + .modal-content { + width: 28%; + } +} + +@media (min-width: 600px) and (max-width: 992px) { + .instancelist li { + width: 60%; + } + + .instancelist li:hover { + width: 65%; + } + + .instance, + .placeholder, + h4 { + width: 60%; + } + + .modal-content { + width: 58%; + } +} + +@media (max-width: 600px) { + .instancelist li { + width: 90%; + } + + .instancelist li:hover { + width: 95%; + } + + .instance, + .placeholder, + h4 { + width: 90%; + } + + .api { + display: none; + } + + .modal-content { + width: 88%; + } +} + +.instancelist li { + color: var(--color); + padding: 10px; + border-bottom: 1px solid var(--footer); + margin: 0 auto; + max-height: 1.5em; + overflow: hidden; + min-height: 1.5em; + line-height: 1.5; +} + +.instancelist li:hover { + background-color: var(--instance-list); + cursor: pointer; + max-height: fit-content; +} + +.instancelist li img { + width: 70%; + margin: 0 auto; +} + +@keyframes opacity { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +footer { + text-align: center; + font-size: 12px; + padding: 10px; + color: var(--footer); +} + +.count { + text-decoration: underline dotted var(--color); + cursor: help; + position: relative; + display: inline-block; + text-underline-offset: 2px; +} + +.blocklist { + width: 90%; + text-align: left; + list-style: none; + padding: 1rem; +} + +.blocklist li { + color: var(--color); + white-space: nowrap; + word-wrap: break-word; + overflow: hidden; +} + +.blocklist li:hover { + white-space: inherit; + cursor: pointer; + color: var(--color); +} + +.blocklist li a, +.blocklist li a:hover, +.blocklist li a:visited { + color: var(--color); + text-decoration: underline; +} + +.blockinstance a, +.blockinstance a:hover { + color: var(--color); + text-decoration: underline; + cursor: help; +} + +.blockcount { + font-weight: bolder; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + scrollbar-width: thin; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + position: relative; + background-color: var(--background-color); + margin: 10px auto; + padding: 0; + border: 1px solid var(--footer); + -webkit-animation-name: animatetop; + -webkit-animation-duration: 0.4s; + animation-name: animatetop; + animation-duration: 0.4s; +} + +.modal-header { + padding: 6px; + background-color: var(--color); + color: var(--background-color); +} + +.modal-body { + color: var(--color); + background-color: var(--background-color); + padding: 6px; +} + +.modal-footer { + padding: 6px; + background-color: var(--color); + color: var(--background-color); +} + +.closemodal, +.download, +.capture { + color: var(--footer); + float: right; + font-size: 3rem; + font-weight: bolder; + margin: 0px 15px; +} + +.closemodal:hover, +.closemodal:focus, +.download:hover, +.download:focus, +.capture:hover, +.capture:focus { + color: var(--color); + text-decoration: none; + cursor: pointer; +} + +@keyframes animatetop { + from { + top: -300px; + opacity: 0; + } + + to { + top: 0; + opacity: 1; + } +} + +@keyframes animatebottom { + from { + top: 0; + opacity: 1; + } + + to { + top: -300px; + opacity: 0 + } +} + +h4 { + white-space: nowrap; + overflow: hidden; + position: relative; + border-left: 1px solid var(--background-color); + border-right: 1px solid var(--background-color); + transition: 0.2s; +} + +h4 p { + margin: 0.3rem 0; +} + +h4:hover { + width: 90%; +} + +.bounce { + display: inline-block; + animation: marquee 90s linear infinite; +} + +.bounce:hover { + animation-play-state: paused; +} + +@keyframes marquee { + 0% { + transform: translateX(100vw); + } + + 100% { + transform: translateX(-100%); + } +} + +.loader-content { + z-index: 0; + background-color: rgba(0, 0, 0, 0.4); + width: 100%; + height: 100%; + display: none; + position: fixed; + top: 0; + left: 0; + overflow: auto; + scrollbar-width: thin; +} + +.loader { + background-color: var(--color); + width: fit-content; + margin: 50vh auto; + z-index: 1; + position: relative; + color: var(--background-color); +} \ No newline at end of file diff --git a/front/src/App.js b/front/src/App.js new file mode 100644 index 0000000..e2258ef --- /dev/null +++ b/front/src/App.js @@ -0,0 +1,28 @@ +import { useState } from 'react'; +import './App.css'; +import Title from './component/Title'; +import Bar from './component/Bar'; +import Count from './component/Count'; +import Form from './component/Form'; +import Scan from './component/Scan'; +import Footer from './component/Footer'; +import Loader from './component/Loader'; + +function App() { + const [searchTerm, setSearchTerm] = useState(''), + [matrix, setMatrix] = useState('off') + return ( + <> + + <Bar setSearch={d => setSearchTerm(d)} /> + <Count /> + <Form searchTerm={searchTerm} matrix={matrix} /> + <Scan /> + <hr /> + <Footer setCurrentMatrix={m => setMatrix(m)} /> + <Loader /> + </> + ); +} + +export default App; diff --git a/front/src/App.test.js b/front/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/front/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(<App />); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/front/src/component/Bar.js b/front/src/component/Bar.js new file mode 100644 index 0000000..dab139c --- /dev/null +++ b/front/src/component/Bar.js @@ -0,0 +1,29 @@ +import { useEffect, useState, useCallback } from 'react'; +import dayjs from '../../node_modules/dayjs/'; +import relativeTime from '../../node_modules/dayjs/plugin/relativeTime'; +dayjs.extend(relativeTime) + +const Bar = (prop) => { + const [bounce, setBounce] = useState([]), + fillBounce = useCallback(async () => { + try { + const response = await fetch('/api/outbox'), + result = await response.json() + if (result && result.length > 0) { + setBounce(result) + } + } catch (e) { + console.error(e) + } + }) + useEffect(() => { + fillBounce() + }, []) + return ( + <h4> + <p className="bounce">{bounce && bounce.length > 0 ? bounce.map(b => <a onClick={() => prop.setSearch(b.content.replace(/.*#/g, '').replace(/\<\/a\>\<\/p\>/, ''))}>{b.content.replace(/<[^>]*>/g, '').replace(new RegExp(new URL(window.location.href).host), '')} - {dayjs().to(b.published)}</a>).reduce((prev, curr) => [prev, ' | ', curr]) : []}</p> + </h4> + ) +} + +export default Bar; \ No newline at end of file diff --git a/front/src/component/Count.js b/front/src/component/Count.js new file mode 100644 index 0000000..facdcff --- /dev/null +++ b/front/src/component/Count.js @@ -0,0 +1,41 @@ +import { useEffect, useState, useCallback } from 'react'; + +const Count = () => { + const [count, setCount] = useState('0'), + [statsres, setStatsres] = useState(''), + fillStats = useCallback(async () => { + try { + const [res, stats] = await Promise.all([(await fetch('/api/count')).json(), (await fetch('/api/stats')).json()]) + setCount(res.count) + setStatsres(stats) + } catch (e) { + console.error(e) + } + }) + useEffect(() => { + (async () => { + await fillStats() + })() + }, []) + return ( + <h3>Search in <a href="/api/stats" target="_blank"> + <span className="count">{count}<div className="tooltip"> + <u><strong><center>STATS</center></strong></u> + <ul><li>Statuses AVG: {Math.round(statsres.status_avg)}</li> + <li>Statuses MAX: {statsres.status_max}</li> + <li>Domain AVG: {Math.round(statsres.domain_avg)}</li> + <li>Domain MAX: {statsres.domain_max}</li> + <li>Users AVG: {Math.round(statsres.user_avg)}</li> + <li>Users MAX: {statsres.user_max}</li> + <li>Stats Instances: {statsres.stats_filtered}</li> + <li>Total Instances: {statsres.instance_count}</li> + <li>Users by Instance: {(Math.round(statsres.user_avg) / statsres.instance_count).toFixed(2)}</li> + <li>Statuses by Domain: {(Math.round(statsres.status_avg) / Math.round(statsres.domain_avg)).toFixed(2)}</li> + <li>Statuses by User: {(Math.round(statsres.status_avg) / Math.round(statsres.user_avg)).toFixed(2)}</li> + </ul></div></span> + </a> public instances<span className="api"> - <a href="/api-docs" target="_blank">API docs</a></span> + </h3> + ) +} + +export default Count; \ No newline at end of file diff --git a/front/src/component/Footer.js b/front/src/component/Footer.js new file mode 100644 index 0000000..45b7e86 --- /dev/null +++ b/front/src/component/Footer.js @@ -0,0 +1,67 @@ +import { useEffect, useState, useCallback } from 'react'; +import Messenger from '../random-text'; + +const Footer = (prop) => { + const [theme, setTheme] = useState('dark'), + [matrix, setMatrix] = useState('off'), + [served, setServed] = useState({ served: 0, lastscan: 0, server: 0, instances: 0, peers: 0, created: 0, updated: 0 }), + loading = () => { + document.querySelector('.loader-content').style.display = 'initial' + setTimeout(() => { + document.querySelector('.loader-content').style.display = 'none' + }, 60 * 1000) + }, + toggleTheme = useCallback(() => { + let tm = document.documentElement.dataset.theme + if (!theme || tm === 'light') { + setTheme('dark') + } else { + setTheme('light') + } + }), + refreshServed = useCallback(async () => { + try { + const response = await fetch('/api/served') + if (response.ok) { + setServed(await response.json()) + } + } catch (e) { + console.error(e) + } + }), + toggleMatrix = useCallback(() => { + if (matrix === 'off') { + setMatrix('on') + prop.setCurrentMatrix('on') + } else { + setMatrix('off') + prop.setCurrentMatrix('off') + } + }) + useEffect(() => { + setTheme(theme) + document.documentElement.dataset.theme = theme + refreshServed() + if (matrix === 'on') { + var walker = document.createTreeWalker(document.getElementById('root'), NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + }, [theme]) + return ( + <footer>Served <span className="served">{served.served}</span> times - Last scan <span className="lastscan">{served.lastscan}</span> peers of <span + className="server">{served.server}</span><br /> + Total scanned <span id="instances">{served.instances}</span> instances with <span className="peers">{served.peers}</span> peers - <span + className="created">{served.created}</span> created - <span className="updated">{served.updated}</span> updated<br /> + matrix <a className="matrix" onClick={() => toggleMatrix()}>{matrix}</a> - download json <a className="download_index" href="/api/download_index" + onClick={() => loading()} download="fediblock-index.json.gz" target="_blank">index</a> - + by <a href="https://about.manalejandro.com" target="_blank">ale</a> <s>©</s>2025  + <a className="darklight" onClick={() => toggleTheme()}>{!theme || theme === 'dark' ? '☼' : '☽'}</a> + </footer> + ) +} + +export default Footer; \ No newline at end of file diff --git a/front/src/component/Form.js b/front/src/component/Form.js new file mode 100644 index 0000000..5ff83ff --- /dev/null +++ b/front/src/component/Form.js @@ -0,0 +1,167 @@ +import { useEffect, useState, useCallback, useRef } from 'react'; +import Modal from './Modal'; +import Messenger from '../random-text'; + +const Form = (prop) => { + let csv + const [searchTerm, setSearchTerm] = useState(''), + [list, setList] = useState([]), + [typeList, setTypeList] = useState('ranking'), + [domain, setDomain] = useState({ domain: '' }), + [reverse, setReverse] = useState(false), + refList = useRef(null), + refDownload = useRef(null), + loading = () => { + document.querySelector('.loader-content').style.display = 'initial' + }, + download = () => { + if (csv.split('\n').length > 2) { + refDownload.current.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' })) + refDownload.current.download = 'fediblock-top100.csv' + } + }, + listinstance = useCallback(async content => { + loading() + if (content && content.length > 0) { + try { + const result = await fetch('/api/list/' + content), + res = await result.json() + if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) { + setTypeList('list') + setList(res) + document.querySelector('.loader-content').style.display = 'none' + } else { + document.querySelector('.loader-content').style.display = 'none' + window.alert('Error: No response') + } + } catch (e) { + document.querySelector('.loader-content').style.display = 'none' + window.alert('Error: ' + e.message) + } + } else { + document.querySelector('.loader-content').style.display = 'none' + } + }), + ranking = useCallback(async () => { + loading() + try { + const result = await fetch('/api/ranking'), + res = await result.json() + if (Array.isArray(res) && res.length > 0) { + setTypeList('ranking') + setList(res) + document.querySelector('.loader-content').style.display = 'none' + } else { + document.querySelector('.loader-content').style.display = 'none' + window.alert('Error: No response') + } + } catch (e) { + document.querySelector('.loader-content').style.display = 'none' + window.alert('Error: ' + e.message) + } + }), + filterKeys = useCallback(event => { + if (event.key && !event.ctrlKey && !event.altKey) { + if ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && event.target.value !== '')) { + if (event.key === '.' && event.target.value === '') { + return + } else if (event.key === 'Backspace' && event.target.value !== '') { + setSearchTerm(searchTerm.substring(0, searchTerm.length - 1)) + } else { + setSearchTerm(searchTerm + event.key) + } + } + } + }) + useEffect(() => { + if (searchTerm && searchTerm.length > 0) { + listinstance(searchTerm) + window.location.hash = searchTerm + } else { + (async () => { + await ranking() + })() + } + if (prop.matrix === 'on') { + var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + }, [searchTerm, prop.matrix]) + useEffect(() => { + if (prop.searchTerm && prop.searchTerm.length > 0) { + setSearchTerm(prop.searchTerm) + } + }, [prop.searchTerm]) + useEffect(() => { + if (window.location.hash && window.location.hash !== '#') { + setSearchTerm(window.location.hash.substring(1)) + } + }, []) + return ( + <> + <section> + <input + autoFocus + type="text" + autoComplete="off" + className="instance" + placeholder="Type the name of the instance" + onKeyUp={(e) => filterKeys(e)} + value={searchTerm} /> + <button className="reverse" title="Reverse search..." onClick={() => { + if (searchTerm && searchTerm.length > 0) { + setReverse(true) + setDomain({ domain: searchTerm }) + loading() + } + }}>Reverse</button> + <div className="placeholder">{typeList === 'list' ? list.suggests && list.suggests.length > 0 ? list.suggests.map(suggest => (<a onClick={() => setSearchTerm(suggest)}>{suggest}</a>)).reduce((prev, curr) => [prev, ', ', curr]) : [] : []}</div> + <ul className="instancelist" ref={refList}>{typeList === 'list' ? list.instances.map(r => + (<li><a onClick={() => { + if (r.blocks) { + setReverse(false) + setDomain({ domain: r.domain }) + loading() + } else { + var a = document.createElement('a') + a.href = '/api/detail_api/' + r.domain + a.title = 'API info for ' + r.domain + a.target = '_blank' + a.dispatchEvent(new MouseEvent('click')) + } + }}>{r.domain}</a> + <span dangerouslySetInnerHTML={{ __html: r.blocks ? ` - ` + r.blocks + ` blocks ` : ` ` }}></span> + <span dangerouslySetInnerHTML={{ + __html: r.nodeinfo ? `<a href="/api/detail_nodeinfo/${r.domain}" title="Nodeinfo for ${r.domain}" target="_blank">ⓘ</a>` + `<br />` : `<br />` + }}></span> + <span dangerouslySetInnerHTML={{ + __html: r.api?.title ? `<br /><br />` + r.api.title + ` - ` + r.api.uri + `<br />` : `<br /><br />` + }}></span> + <span dangerouslySetInnerHTML={{ __html: r.last ? `Last update: ` + (new Date(r.last)).toLocaleString() + `<br />` : `` }}></span> + <span dangerouslySetInnerHTML={{ __html: r.api?.email ? `Email: ` + r.api.email + `<br />` : `` }}></span> + <span dangerouslySetInnerHTML={{ __html: `Registration: ` + (r.api?.registrations ? `open` : `closed`) + ` - Version: ` + r.api?.version + `<br />` }}></span> + <span dangerouslySetInnerHTML={{ __html: r.api?.stats ? `Users: ` + r.api.stats.user_count + ` - Statuses: ` + r.api.stats.status_count + ` - Domains: ` + r.api.stats.domain_count + `<br />` : `` }}></span> + <span dangerouslySetInnerHTML={{ __html: r.api?.description ? `Description: ` + r.api.description + `<br />` : `` }}></span> + <span dangerouslySetInnerHTML={{ __html: r.api?.thumbnail ? `<img domain="${r.domain}" loading="lazy" src="${r.api.thumbnail}" />` + `<br />` : `` }}></span> + </li> + )) : typeList === 'ranking' ? list.map((r, i) => { + if (i === 0) { + csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n' + return (<><li><strong>Top 100</strong><br /><small className="download-top"><a ref={refDownload} onClick={() => download()}>(Import CSV)</a></small></li><li>{i + 1} - {r.domain} - {r.count} blocks</li></>) + } else { + csv += !r.domain.match(/\*/) ? r.domain + ',suspend,False,False,"suspended by top 100 of ' + new URL(window.location.href).host + '",False\n' : '' + return (<li>{i + 1} - {r.domain} - {r.count} blocks</li>) + } + }) : ''} + </ul> + </section > + <Modal domain={domain} reverse={reverse} setSearch={d => setSearchTerm(d)} matrix={prop.matrix} /> + </> + ) +} + +export default Form; \ No newline at end of file diff --git a/front/src/component/Loader.js b/front/src/component/Loader.js new file mode 100644 index 0000000..bba45dc --- /dev/null +++ b/front/src/component/Loader.js @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react'; +import '../loaders.css'; + +const Loader = () => { + const loaders = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario'], + [load, setLoad] = useState('load ' + loaders[Math.floor(Math.random() * loaders.length)]) + useEffect(() => { + setLoad('load ' + loaders[Math.floor(Math.random() * loaders.length)]) + }, []) + return ( + <div className="loader-content"> + <div className="loader"> + <div className={load}></div> + </div> + </div> + ) +} + +export default Loader; \ No newline at end of file diff --git a/front/src/component/Modal.js b/front/src/component/Modal.js new file mode 100644 index 0000000..4d7b833 --- /dev/null +++ b/front/src/component/Modal.js @@ -0,0 +1,142 @@ +import { useEffect, useCallback, useState, useRef } from 'react'; +import html2canvas from '../../node_modules/html2canvas/dist/html2canvas.js'; +import Messenger from '../random-text.js'; + +const Modal = (prop) => { + let csv + const [blocktitle, setBlocktitle] = useState(''), + [blockcount, setBlockcount] = useState(''), + [blockinstance, setBlockinstance] = useState(''), + [blocktook, setBlocktook] = useState(''), + [blocklist, setBlocklist] = useState([]), + hrefCanvas = useRef(null), + refList = useRef(null), + loading = () => { + document.querySelector('.loader-content').style.display = 'initial' + }, + closeModal = event => { + if (event.target.classList.contains('modal') || event.target.classList.contains('closemodal')) { + document.querySelector('.modal-content').style.animationName = 'animatebottom' + } + }, + animationEnd = event => { + if (event.animationName === 'animatebottom') { + document.querySelector('.modal').style.display = 'none' + } + }, + capture = useCallback(async () => { + const canvas = await html2canvas(hrefCanvas.current, { useCORS: true }), + capture = document.querySelector('.capture') + capture.download = 'fediblock-' + Date.now() + '.png' + capture.href = canvas.toDataURL('image/png', 1.0) + capture.dispatchEvent(new MouseEvent('click')) + }), + download = () => { + const download = document.querySelector('.download') + if (csv.split('\n').length > 2) { + download.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' })) + download.download = 'fediblock-' + prop.domain.domain + '.csv' + } + }, + reverse = useCallback(async content => { + if (content && content.length > 0) { + loading() + const result = await fetch('/api/block_count/' + content), + res = await result.json() + if (res && Array.isArray(res.instances)) { + setBlocktitle('Reverse List') + setBlockcount(res.block_count) + setBlockinstance('ing ' + content) + setBlocktook('took ' + res.took + 'ms') + setBlocklist(res.instances) + document.querySelector('.loader-content').style.display = 'none' + document.querySelector('.modal-content').style.animationName = 'animatetop' + document.querySelector('.modal').style.display = 'block' + if (prop.matrix === 'on') { + var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + } + } + }), + listblock = useCallback(async (domain) => { + const result = await fetch('/api/detail/' + domain), + res = await result.json() + if (res.blocks && Array.isArray(res.blocks)) { + csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n' + setBlocktitle('Blocked List') + setBlockcount(res.blocks.length) + setBlocktook('took ' + res.took + 'ms') + setBlockinstance(`ed by ` + (res.api ? `<a href="/api/detail_api/${res.instance}" title="API info for ${res.instance}" target="_blank">${res.instance}</a>` + : res.instance) + (res.nodeinfo ? ` <a href="/api/detail_nodeinfo/${res.instance}" title="Nodeinfo for ${res.instance}" target="_blank">ⓘ</a><br />` : `<br />`) + + `Last update: ` + (new Date(res.last)).toLocaleString()) + setBlocklist(res.blocks) + if (prop.matrix === 'on') { + var walker = document.createTreeWalker(refList.current, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + } else { + var a = document.createElement('a') + a.href = '/api/detail_api/' + domain + a.title = 'API info for ' + domain + a.target = '_blank' + a.dispatchEvent(new MouseEvent('click')) + } + document.querySelector('.loader-content').style.display = 'none' + document.querySelector('.modal-content').style.animationName = 'animatetop' + document.querySelector('.modal').style.display = 'block' + }) + useEffect(() => { + if (prop.domain && prop.domain.domain.length > 0) { + if (prop.reverse) { + reverse(prop.domain.domain) + } else { + listblock(prop.domain.domain) + } + } + }, [prop.domain, prop.reverse]) + return ( + <div className="modal" onClick={(e) => closeModal(e)}> + <div className="modal-content" onAnimationEnd={(e) => animationEnd(e)} ref={refList}> + <div className="modal-header"> + <span className="closemodal" title="Close" onClick={(e) => closeModal(e)}>×</span> + <a className="capture" title="Take snapshot" onClick={capture} type="image/png" target="_blank">📷</a> + {!prop.reverse ? <a className="download" title="Download fediblock CSV mastodon file" onClick={download} type="text/csv" target="_blank">⇩</a> : ''} + <h2>{blocktitle}</h2> + </div> + <div className="modal-body" ref={hrefCanvas}> + <p> + <span className="blockcount">{blockcount}</span> public instances are block<span className="blockinstance" dangerouslySetInnerHTML={{ __html: blockinstance }}></span> + <br /><small className="blocktook">{blocktook}</small> + </p> + <ul className="blocklist">{prop.reverse ? blocklist.map((instance, index) => { + return (<li>{index + 1}. <a onClick={() => { + prop.setSearch(instance.instance) + document.querySelector('.modal-content').style.animationName = 'animatebottom' + }}>{instance.instance}</a>{instance.comment ? ' - ' + instance.comment : ''}</li>) + }) : blocklist.map((r, i) => { + if (r?.domain) { + csv += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : '' + return (<li>{i + 1}. <a onClick={() => { + prop.setSearch(r.domain) + document.querySelector('.modal-content').style.animationName = 'animatebottom' + }}>{r.domain}</a>{r.severity ? ' - ' + r.severity : ''}{r.comment ? ' - ' + r.comment : ''}</li>) + } + })}</ul> + </div> + <div className="modal-footer"> + </div> + </div> + </div> + ) +} + +export default Modal; \ No newline at end of file diff --git a/front/src/component/Scan.js b/front/src/component/Scan.js new file mode 100644 index 0000000..acc3ba2 --- /dev/null +++ b/front/src/component/Scan.js @@ -0,0 +1,31 @@ +import { useEffect, useRef, useState } from 'react'; + +const Scan = () => { + const [scan, setScan] = useState('Scanning...'), + load = useRef(false) + useEffect(() => { + if (!load.current) { + const source = new window.EventSource('/api/scan') + source.onmessage = event => { + if (event.data) { + if (event.data.length > 0) { + setScan('Async Scanning' + event.data) + } else { + setScan('Async Scanning...') + } + } + } + source.onerror = error => { + console.error(error) + } + load.current = true + } + }) + return ( + <div> + <span className="scan">{scan}</span> + </div> + ) +} + +export default Scan; \ No newline at end of file diff --git a/front/src/component/Title.js b/front/src/component/Title.js new file mode 100644 index 0000000..17488f0 --- /dev/null +++ b/front/src/component/Title.js @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; + +const Title = () => { + const click = useCallback(() => { + window.location.hash = '' + window.location.reload(false) + }) + return ( + <h1><a onClick={() => click()} className="title">Fediblock Instance Φ</a></h1> + ) +} + +export default Title; \ No newline at end of file diff --git a/front/src/index.js b/front/src/index.js new file mode 100644 index 0000000..b62b96a --- /dev/null +++ b/front/src/index.js @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + <React.StrictMode> + <App /> + </React.StrictMode> +); +reportWebVitals(); diff --git a/front/src/loaders.css b/front/src/loaders.css new file mode 100644 index 0000000..c1ec51c --- /dev/null +++ b/front/src/loaders.css @@ -0,0 +1,411 @@ +/* HTML: <div class="loader"></div> */ +.loader-pong { + width: 80px; + height: 70px; + border: 5px solid #000; + padding: 0 8px; + box-sizing: border-box; + background: + linear-gradient(#fff 0 0) 0 0/8px 20px, + linear-gradient(#fff 0 0) 100% 0/8px 20px, + radial-gradient(farthest-side, #fff 90%, #0000) 0 5px/8px 8px content-box, + #000; + background-repeat: no-repeat; + animation: l3 2s infinite linear; +} + +@keyframes l3 { + 25% { + background-position: 0 0, 100% 100%, 100% calc(100% - 5px) + } + + 50% { + background-position: 0 100%, 100% 100%, 0 calc(100% - 5px) + } + + 75% { + background-position: 0 100%, 100% 0, 100% 5px + } +} + +/* HTML: <div class="loader"></div> */ +.loader-pacman { + width: 90px; + height: 24px; + padding: 2px 0; + box-sizing: border-box; + display: flex; + animation: l5-0 3s infinite steps(6); + background: + linear-gradient(#000 0 0) 0 0/0% 100% no-repeat, + radial-gradient(circle 3px, #eeee89 90%, #0000) 0 0/20% 100% #000; + overflow: hidden; +} + +.loader-pacman::before { + content: ""; + width: 20px; + transform: translate(-100%); + border-radius: 50%; + background: #ffff2d; + animation: + l5-1 .25s .153s infinite steps(5) alternate, + l5-2 3s infinite linear; +} + +@keyframes l5-1 { + 0% { + clip-path: polygon(50% 50%, 100% 0, 100% 0, 0 0, 0 100%, 100% 100%, 100% 100%) + } + + 100% { + clip-path: polygon(50% 50%, 100% 65%, 100% 0, 0 0, 0 100%, 100% 100%, 100% 35%) + } +} + +@keyframes l5-2 { + 100% { + transform: translate(90px) + } +} + +@keyframes l5-0 { + 100% { + background-size: 120% 100%, 20% 100% + } +} + +/* HTML: <div class="loader"></div> */ +.loader-abyss { + width: 80px; + height: 60px; + box-sizing: border-box; + background: + linear-gradient(#fff 0 0) left /calc(50% - 15px) 8px no-repeat, + linear-gradient(#fff 0 0) right/calc(50% - 15px) 8px no-repeat, + conic-gradient(from 135deg at top, #0000, red 1deg 90deg, #0000 91deg) bottom/14px 8px repeat-x, + #000; + border-bottom: 2px solid red; + position: relative; + overflow: hidden; + animation: l6-0 1s infinite linear; +} + +.loader-abyss::before { + content: ""; + position: absolute; + width: 10px; + height: 14px; + background: lightblue; + left: -5px; + animation: + l6-1 2s infinite cubic-bezier(0, 100, 1, 100), + l6-2 2s infinite linear; +} + +@keyframes l6-0 { + 50% { + background-position: left, right, bottom -2px left -4px + } +} + +@keyframes l6-1 { + + 0%, + 27% { + bottom: calc(50% + 4px) + } + + 65%, + 100% { + bottom: calc(50% + 4.1px) + } +} + +@keyframes l6-2 { + 100% { + left: 100% + } +} + +/* HTML: <div class="loader"></div> */ +.loader-jump { + width: 70px; + height: 50px; + box-sizing: border-box; + background: + conic-gradient(from 135deg at top, #0000, #fff 1deg 90deg, #0000 91deg) right -20px bottom 8px/18px 9px, + linear-gradient(#fff 0 0) bottom/100% 8px, + #000; + background-repeat: no-repeat; + border-bottom: 8px solid #000; + position: relative; + animation: l7-0 2s infinite linear; +} + +.loader-jump::before { + content: ""; + position: absolute; + width: 10px; + height: 14px; + background: lightblue; + left: 10px; + animation: l7-1 2s infinite cubic-bezier(0, 200, 1, 200); +} + +@keyframes l7-0 { + 100% { + background-position: left -20px bottom 8px, bottom + } +} + +@keyframes l7-1 { + + 0%, + 50% { + bottom: 8px + } + + 90%, + 100% { + bottom: 8.1px + } +} + +/* HTML: <div class="loader"></div> */ +.loader-loading { + width: fit-content; + font-size: 17px; + font-family: monospace; + line-height: 1.4; + font-weight: bold; + --c: no-repeat linear-gradient(#000 0 0); + background: var(--c), var(--c), var(--c), var(--c), var(--c), var(--c), var(--c); + background-size: calc(1ch + 1px) 100%; + border-bottom: 10px solid #0000; + position: relative; + animation: l8-0 3s infinite linear; + clip-path: inset(-20px 0); +} + +.loader-loading::before { + content: "Loading"; +} + +.loader-loading::after { + content: ""; + position: absolute; + width: 10px; + height: 14px; + background: #25adda; + left: -10px; + bottom: 100%; + animation: l8-1 3s infinite linear; +} + +@keyframes l8-0 { + + 0%, + 12.5% { + background-position: calc(0*100%/6) 0, calc(1*100%/6) 0, calc(2*100%/6) 0, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0 + } + + 25% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 0, calc(2*100%/6) 0, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0 + } + + 37.5% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 0, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0 + } + + 50% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 0, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0 + } + + 62.5% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 0, calc(5*100%/6) 0, calc(6*100%/6) 0 + } + + 75% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 40px, calc(5*100%/6) 0, calc(6*100%/6) 0 + } + + 87.4% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 40px, calc(5*100%/6) 40px, calc(6*100%/6) 0 + } + + 100% { + background-position: calc(0*100%/6) 40px, calc(1*100%/6) 40px, calc(2*100%/6) 40px, calc(3*100%/6) 40px, calc(4*100%/6) 40px, calc(5*100%/6) 40px, calc(6*100%/6) 40px + } +} + +@keyframes l8-1 { + 100% { + left: 115% + } +} + +/* HTML: <div class="loader"></div> */ +.loader-avenger { + width: fit-content; + font-size: 17px; + font-family: monospace; + line-height: 1.4; + font-weight: bold; + background: + linear-gradient(#000 0 0) left, + linear-gradient(#000 0 0) right; + background-repeat: no-repeat; + border-right: 5px solid #0000; + border-left: 5px solid #0000; + background-origin: border-box; + position: relative; + animation: l9-0 2s infinite; +} + +.loader-avenger::before { + content: "Loading"; +} + +.loader-avenger::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + width: 22px; + height: 60px; + background: + linear-gradient(90deg, #000 4px, #0000 0 calc(100% - 4px), #000 0) bottom /22px 20px, + linear-gradient(90deg, red 4px, #0000 0 calc(100% - 4px), red 0) bottom 10px left 0/22px 6px, + linear-gradient(#000 0 0) bottom 3px left 0 /22px 8px, + linear-gradient(#000 0 0) bottom 0 left 50%/8px 16px; + background-repeat: no-repeat; + animation: l9-1 2s infinite; +} + +@keyframes l9-0 { + + 0%, + 25% { + background-size: 50% 100% + } + + 25.1%, + 75% { + background-size: 0 0, 50% 100% + } + + 75.1%, + 100% { + background-size: 0 0, 0 0 + } +} + +@keyframes l9-1 { + 25% { + background-position: bottom, bottom 54px left 0, bottom 3px left 0, bottom 0 left 50%; + left: 0 + } + + 25.1% { + background-position: bottom, bottom 10px left 0, bottom 3px left 0, bottom 0 left 50%; + left: 0 + } + + 50% { + background-position: bottom, bottom 10px left 0, bottom 3px left 0, bottom 0 left 50%; + left: calc(100% - 22px) + } + + 75% { + background-position: bottom, bottom 54px left 0, bottom 3px left 0, bottom 0 left 50%; + left: calc(100% - 22px) + } + + 75.1% { + background-position: bottom, bottom 10px left 0, bottom 3px left 0, bottom 0 left 50%; + left: calc(100% - 22px) + } +} + +/* HTML: <div class="loader"></div> */ +.loader-mario { + width: fit-content; + font-size: 17px; + font-family: monospace; + line-height: 1.4; + font-weight: bold; + padding: 30px 2px 50px; + background: linear-gradient(#000 0 0) 0 0/100% 100% content-box padding-box no-repeat; + position: relative; + overflow: hidden; + animation: l10-0 2s infinite cubic-bezier(1, 175, .5, 175); +} + +.loader-mario::before { + content: "Loading"; + display: inline-block; + animation: l10-2 2s infinite; +} + +.loader-mario::after { + content: ""; + position: absolute; + width: 34px; + height: 28px; + top: 110%; + left: calc(50% - 16px); + background: + linear-gradient(90deg, #0000 12px, #f92033 0 22px, #0000 0 26px, #fdc98d 0 32px, #0000) bottom 26px left 50%, + linear-gradient(90deg, #0000 10px, #f92033 0 28px, #fdc98d 0 32px, #0000 0) bottom 24px left 50%, + linear-gradient(90deg, #0000 10px, #643700 0 16px, #fdc98d 0 20px, #000 0 22px, #fdc98d 0 24px, #000 0 26px, #f92033 0 32px, #0000 0) bottom 22px left 50%, + linear-gradient(90deg, #0000 8px, #643700 0 10px, #fdc98d 0 12px, #643700 0 14px, #fdc98d 0 20px, #000 0 22px, #fdc98d 0 28px, #f92033 0 32px, #0000 0) bottom 20px left 50%, + linear-gradient(90deg, #0000 8px, #643700 0 10px, #fdc98d 0 12px, #643700 0 16px, #fdc98d 0 22px, #000 0 24px, #fdc98d 0 30px, #f92033 0 32px, #0000 0) bottom 18px left 50%, + linear-gradient(90deg, #0000 8px, #643700 0 12px, #fdc98d 0 20px, #000 0 28px, #f92033 0 30px, #0000 0) bottom 16px left 50%, + linear-gradient(90deg, #0000 12px, #fdc98d 0 26px, #f92033 0 30px, #0000 0) bottom 14px left 50%, + linear-gradient(90deg, #fdc98d 6px, #f92033 0 14px, #222a87 0 16px, #f92033 0 22px, #222a87 0 24px, #f92033 0 28px, #0000 0 32px, #643700 0) bottom 12px left 50%, + linear-gradient(90deg, #fdc98d 6px, #f92033 0 16px, #222a87 0 18px, #f92033 0 24px, #f92033 0 26px, #0000 0 30px, #643700 0) bottom 10px left 50%, + linear-gradient(90deg, #0000 10px, #f92033 0 16px, #222a87 0 24px, #feee49 0 26px, #222a87 0 30px, #643700 0) bottom 8px left 50%, + linear-gradient(90deg, #0000 12px, #222a87 0 18px, #feee49 0 20px, #222a87 0 30px, #643700 0) bottom 6px left 50%, + linear-gradient(90deg, #0000 8px, #643700 0 12px, #222a87 0 30px, #643700 0) bottom 4px left 50%, + linear-gradient(90deg, #0000 6px, #643700 0 14px, #222a87 0 26px, #0000 0) bottom 2px left 50%, + linear-gradient(90deg, #0000 6px, #643700 0 10px, #0000 0) bottom 0px left 50%; + background-size: 34px 2px; + background-repeat: no-repeat; + animation: inherit; + animation-name: l10-1; +} + +@keyframes l10-0 { + + 0%, + 30% { + background-position: 0 0px + } + + 50%, + 100% { + background-position: 0 -0.1px + } +} + +@keyframes l10-1 { + + 50%, + 100% { + top: 109.5% + } +} + +@keyframes l10-2 { + + 0%, + 30% { + transform: translateY(0); + } + + 80%, + 100% { + transform: translateY(-260%); + } +} \ No newline at end of file diff --git a/front/src/logo.svg b/front/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/front/src/logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file diff --git a/front/src/random-text.js b/front/src/random-text.js new file mode 100644 index 0000000..b96682e --- /dev/null +++ b/front/src/random-text.js @@ -0,0 +1,69 @@ +var Messenger = function (el) { + 'use strict'; + var m = this; + + m.init = function () { + m.codeletters = "&#*+%?£@§$"; + m.current_length = 0; + m.fadeBuffer = false; + m.message = el.textContent.length > 0 ? el.textContent : '' + + setTimeout(m.animateIn, 300); + }; + + m.generateRandomString = function (length) { + var random_text = ''; + while (random_text.length < length) { + random_text += m.codeletters.charAt(Math.floor(Math.random() * m.codeletters.length)); + } + + return random_text; + }; + + m.animateIn = function () { + if (m.current_length < m.message.length) { + m.current_length = m.current_length + 2; + if (m.current_length > m.message.length) { + m.current_length = m.message.length; + } + + var message = m.generateRandomString(m.current_length); + el.textContent = message; + + setTimeout(m.animateIn, 60); + } else { + setTimeout(m.animateFadeBuffer, 60); + } + }; + + m.animateFadeBuffer = function () { + if (m.fadeBuffer === false) { + m.fadeBuffer = []; + for (var i = 0; i < m.message.length; i++) { + m.fadeBuffer.push({ c: (Math.floor(Math.random() * 12)) + 1, l: m.message.charAt(i) }); + } + } + + var do_cycles = false; + var message = ''; + + for (var i = 0; i < m.fadeBuffer.length; i++) { + var fader = m.fadeBuffer[i]; + if (fader.c > 0) { + do_cycles = true; + fader.c--; + message += m.codeletters.charAt(Math.floor(Math.random() * m.codeletters.length)); + } else { + message += fader.l; + } + } + + el.textContent = message; + + setTimeout(m.animateFadeBuffer, 150); + }; + + m.init(); +} + +module.exports = Messenger; \ No newline at end of file diff --git a/front/src/reportWebVitals.js b/front/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/front/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/front/src/setupTests.js b/front/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/front/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom';