From e0fae7a6b4c0a9ea77fbde7376431f96a78f742b Mon Sep 17 00:00:00 2001 From: ale Date: Sun, 15 Sep 2024 19:44:53 +0200 Subject: [PATCH] initial commit --- .drone.yml | 40 + .gitignore | 5 + .gitlab-ci.yml | 14 + Dockerfile | 8 + README.md | 42 + docker-compose.yml | 58 ++ elasticsearch-data/.git-keep | 0 fediblock-mapping.json | 1799 ++++++++++++++++++++++++++++++++++ lib/apex.js | 21 + lib/apexcustom.js | 105 ++ lib/api.js | 53 + lib/apiswagger.js | 489 +++++++++ lib/constant.js | 14 + lib/fediblock.js | 261 +++++ lib/logger.js | 7 + lib/swagger.js | 25 + lib/util.js | 45 + lib/worker-fediblock.js | 106 ++ package.json | 31 + public/favicon.ico | Bin 0 -> 31014 bytes public/index.html | 77 ++ public/loaders.css | 413 ++++++++ public/main.css | 378 +++++++ public/main.js | 418 ++++++++ public/random-text.js | 69 ++ served.txt | 1 + server.js | 146 +++ 27 files changed, 4625 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 elasticsearch-data/.git-keep create mode 100644 fediblock-mapping.json create mode 100644 lib/apex.js create mode 100644 lib/apexcustom.js create mode 100644 lib/api.js create mode 100644 lib/apiswagger.js create mode 100644 lib/constant.js create mode 100644 lib/fediblock.js create mode 100644 lib/logger.js create mode 100644 lib/swagger.js create mode 100644 lib/util.js create mode 100644 lib/worker-fediblock.js create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 public/loaders.css create mode 100644 public/main.css create mode 100644 public/main.js create mode 100644 public/random-text.js create mode 100644 served.txt create mode 100644 server.js diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..be2add2 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,40 @@ +kind: pipeline +name: build-linux-amd64 +type: docker + +platform: + os: linux + arch: arm64 + +steps: +- name: build + image: docker:dind + privileged: true + environment: + USER: + from_secret: user + PASS: + from_secret: pass + REGISTRY: + from_secret: registry + volumes: + - name: dockersock + path: /var/run/docker.sock + - name: usrbin + path: /usr/bin + commands: + - docker login -u $USER -p $PASS $REGISTRY + - docker buildx build --platform amd64 -t $REGISTRY/fediblock-instance . + - docker push $REGISTRY/fediblock-instance + when: + event: + - push + - tag + +volumes: +- name: dockersock + host: + path: /var/run/docker.sock +- name: usrbin + host: + path: /usr/bin \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba43a0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.parcel-cache/ +logs/ +dist/ +yarn.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..11f4717 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,14 @@ +image: docker:dind + +services: + - docker:dind + +before_script: + - docker -v + +build image: + stage: build + script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker build -t $CI_REGISTRY/manalejandro/fediblock-instance/fediblock-instance:latest . + - docker push $CI_REGISTRY/manalejandro/fediblock-instance/fediblock-instance:latest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2ec2f3e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:21-slim +RUN apt update && apt install -y git && apt clean +COPY --chown=node:node . /fediblock-instance +USER node +WORKDIR /fediblock-instance +RUN yarn config set network-timeout 300000 && yarn && yarn build +EXPOSE 4000 +ENTRYPOINT ["yarn", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b87f8d --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Fediblock Instance [![Build Status](https://drone.manalejandro.com/api/badges/ale/fediblock-instance/status.svg)](https://drone.manalejandro.com/ale/fediblock-instance) + +## Another instance to search public blocks and do stats and more, like one FBA but with NodeJS powers. + +## We need one mongodb instance for federation and one elasticsearch node to storage data. + +## Install + +``` +$ yarn or npm i +``` + +## Build frontend + +``` +$ yarn build or npm run build +``` + +## Configuration + +- edit `lib/constant.js` with your environment preferences + +## Start + +``` +$ yarn start or npm run start +``` + +## Docker + +``` +$ docker compose build or docker pull registry.gitlab.com/manalejandro/fediblock-instance/fediblock-instance:latest +$ docker compose pull +$ docker compose up -d +``` + +### Best deploy with subdomain in one HTTPS proxy like nginx or apache throught 4000/tcp port + + +## License + +MIT \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6328db5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: '3' + +services: + fediblock-instance: +# image: registry.gitlab.com/manalejandro/fediblock-instance/fediblock-instance:latest + image: fediblock-instance + build: . + hostname: fediblock-instance + container_name: fediblock-instance + restart: always + user: node + ports: + - "4000:4000" + depends_on: + - fediblock-mongodb + - fediblock-elasticsearch + networks: + fediblocknet: + + fediblock-mongodb: + image: mongo:4.4.28 + hostname: fediblock-mongodb + container_name: fediblock-mongodb + restart: always + command: --wiredTigerCacheSizeGB 0.5 + volumes: + - ./mongodb:/data/db + networks: + fediblocknet: + + fediblock-elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.13.2-amd64 + hostname: fediblock-elasticsearch + container_name: fediblock-elasticsearch + restart: always + environment: + - node.name=fediblock-elasticsearch + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + - xpack.security.enabled=false + - indices.id_field_data.enabled=true + - discovery.type=single-node + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65535 + hard: 65535 + volumes: + - ./elasticsearch-data:/usr/share/elasticsearch/data + expose: + - 9200 + networks: + fediblocknet: + +networks: + fediblocknet: diff --git a/elasticsearch-data/.git-keep b/elasticsearch-data/.git-keep new file mode 100644 index 0000000..e69de29 diff --git a/fediblock-mapping.json b/fediblock-mapping.json new file mode 100644 index 0000000..9f9b78c --- /dev/null +++ b/fediblock-mapping.json @@ -0,0 +1,1799 @@ +{ + "mappings": { + "properties": { + "api": { + "properties": { + "accent_color": { + "type": "text" + }, + "account_domain": { + "type": "text" + }, + "allow_unauthenticated": { + "properties": { + "timeline_local": { + "type": "boolean" + } + } + }, + "api_schema": { + "type": "text" + }, + "approval_required": { + "type": "boolean" + }, + "authentication_methods": { + "type": "text" + }, + "avatar_upload_limit": { + "type": "long" + }, + "background_image": { + "type": "text" + }, + "background_upload_limit": { + "type": "long" + }, + "banner_upload_limit": { + "type": "long" + }, + "blockchains": { + "properties": { + "chain_id": { + "type": "text" + }, + "features": { + "properties": { + "gate": { + "type": "boolean" + }, + "minter": { + "type": "boolean" + }, + "subscriptions": { + "type": "boolean" + } + } + } + } + }, + "chat_limit": { + "type": "long" + }, + "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" + }, + "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" + } + } + }, + "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" + }, + "avatar": { + "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_static": { + "type": "text" + }, + "header_thumbhash": { + "type": "text" + }, + "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" + }, + "jam_identity": { + "type": "text" + }, + "last_status_at": { + "type": "date" + }, + "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" + }, + "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" + }, + "background_image": { + "type": "text" + }, + "birthday": { + "type": "date" + }, + "favicon": { + "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" + }, + "relationship": { + "type": "object" + }, + "skip_thread_containment": { + "type": "boolean" + }, + "tags": { + "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" + } + } + }, + "note": { + "type": "text" + }, + "pleroma": { + "properties": { + "actor_type": { + "type": "text" + }, + "discoverable": { + "type": "boolean" + } + } + }, + "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" + } + } + }, + "description": { + "type": "text" + }, + "description_limit": { + "type": "long" + }, + "description_source": { + "type": "text" + }, + "description_text": { + "type": "text" + }, + "domain": { + "type": "text" + }, + "email": { + "type": "text" + }, + "errors": { + "properties": { + "detail": { + "type": "text" + } + } + }, + "feature_quote": { + "type": "boolean" + }, + "federated_timeline_restricted": { + "type": "boolean" + }, + "fedibird_capabilities": { + "type": "text" + }, + "icon": { + "type": "text" + }, + "invites_enabled": { + "type": "boolean" + }, + "languages": { + "type": "text" + }, + "login_message": { + "type": "text" + }, + "max_media_attachments": { + "type": "long" + }, + "max_post_chars": { + "type": "long" + }, + "max_toot_chars": { + "type": "long" + }, + "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_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" + } + } + }, + "registrations": { + "type": "boolean" + }, + "rules": { + "properties": { + "hint": { + "type": "text" + }, + "id": { + "type": "text" + }, + "text": { + "type": "text" + } + } + }, + "short_description": { + "type": "text" + }, + "short_description_text": { + "type": "text" + }, + "shout_limit": { + "type": "long" + }, + "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" + } + } + }, + "terms": { + "type": "text" + }, + "terms_text": { + "type": "text" + }, + "thumbnail": { + "type": "text" + }, + "thumbnail_description": { + "type": "text" + }, + "thumbnail_type": { + "type": "text" + }, + "title": { + "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" + } + } + }, + "blocks": { + "type": "nested", + "properties": { + "accountId": { + "type": "keyword" + }, + "accountUid": { + "type": "keyword" + }, + "channelNo": { + "type": "long" + }, + "channelType": { + "type": "keyword" + }, + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "digest": { + "type": "keyword" + }, + "domain": { + "type": "keyword" + }, + "grade": { + "type": "keyword" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "keyword" + }, + "representativeImageUrl": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_ex": { + "type": "keyword" + }, + "stock_auto": { + "type": "boolean" + }, + "update_at": { + "type": "date" + }, + "url": { + "type": "keyword" + } + } + }, + "instance": { + "type": "keyword" + }, + "last": { + "type": "date" + }, + "nodeinfo": { + "properties": { + "_destroyed": { + "type": "boolean" + }, + "error": { + "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" + }, + "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" + }, + "federation": { + "properties": { + "enabled": { + "type": "boolean" + }, + "exclusions": { + "type": "boolean" + }, + "mrf_hashtag": { + "properties": { + "sensitive": { + "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" + }, + "mailerEnabled": { + "type": "boolean" + }, + "maintainer": { + "properties": { + "email": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "maxCaptionTextLength": { + "type": "long" + }, + "maxNoteTextLength": { + "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" + }, + "privacyPolicyUrl": { + "type": "text" + }, + "private": { + "type": "boolean" + }, + "proxyAccountName": { + "type": "text" + }, + "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" + } + } + } + } + }, + "openRegistrations": { + "type": "boolean" + }, + "protocols": { + "type": "text" + }, + "services": { + "properties": { + "inbound": { + "type": "text" + }, + "outbound": { + "type": "text" + } + } + }, + "software": { + "properties": { + "homepage": { + "type": "text" + }, + "name": { + "type": "text" + }, + "version": { + "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" + } + } + } +} \ No newline at end of file diff --git a/lib/apex.js b/lib/apex.js new file mode 100644 index 0000000..3990811 --- /dev/null +++ b/lib/apex.js @@ -0,0 +1,21 @@ +// define routes using prepacakged middleware collections +module.exports = (app, apex, routes) => { + app.route(routes.inbox) + .get(apex.net.inbox.get) + .post(apex.net.inbox.post) + app.route(routes.outbox) + .get(apex.net.outbox.get) + .post(apex.net.outbox.post) + app.get(routes.actor, apex.net.actor.get) + app.get(routes.followers, apex.net.followers.get) + app.get(routes.following, apex.net.following.get) + app.get(routes.liked, apex.net.liked.get) + app.get(routes.object, apex.net.object.get) + app.get(routes.activity, apex.net.activityStream.get) + app.get(routes.shares, apex.net.shares.get) + app.get(routes.likes, apex.net.likes.get) + app.get('/.well-known/webfinger', apex.net.webfinger.get) + app.get('/.well-known/nodeinfo', apex.net.nodeInfoLocation.get) + app.get('/nodeinfo/:version', apex.net.nodeInfo.get) + app.post('/proxy', apex.net.proxy.post) +} diff --git a/lib/apexcustom.js b/lib/apexcustom.js new file mode 100644 index 0000000..0c7c794 --- /dev/null +++ b/lib/apexcustom.js @@ -0,0 +1,105 @@ +// custom side-effects for your app +module.exports = (app, apex, client) => { + const util = require('./util')(apex), + constant = require('./constant'), + https = require('https') + app.on('apex-outbox', msg => { + if (typeof msg === 'object' + && !Object.keys(msg).filter(prop => msg[prop] + && typeof msg[prop] === 'object').some(prop => !(msg[prop].hasOwnProperty('id') + && msg[prop].hasOwnProperty('type'))) + && msg.activity + && msg.activity.type) { + console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`) + } else { + console.log(JSON.stringify(msg, '', 2)) + } + }) + app.on('apex-inbox', async msg => { + if (typeof msg === 'object' + && !Object.keys(msg).filter(prop => msg[prop] + && typeof msg[prop] === 'object').some(prop => !(msg[prop].hasOwnProperty('id') + && msg[prop].hasOwnProperty('type'))) + && msg.activity + && msg.activity.type) { + const type = msg.activity.type.toLowerCase() + if (type === 'follow' && msg.recipient.type.toLowerCase() === 'person') { + const follow = await apex.acceptFollow(msg.recipient, msg.activity) + const act = await apex.buildActivity('Accept', apex.utils.usernameToIRI(constant.nick), ['https://www.w3.org/ns/activitystreams#Public'].concat([msg.actor.id]), { + actor: msg.recipient, + object: msg.activity + }) + await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true), act) + } + if (type === 'create' && msg.object.type.toLowerCase() === 'note' && msg.actor.preferredUsername && msg.object.content) { + const name = Array.isArray(msg.actor.preferredUsername) ? msg.actor.preferredUsername[0] : msg.actor.preferredUsername, + content = ('' + msg.object.content).replace(/<[^>]*>?/gm, '').split(' ').filter(token => !token.startsWith('@')) + if (msg.recipient.id === apex.utils.usernameToIRI(constant.nick)) { + if (content.length === 1) { + const instance = content.join('').trim(), + ac = new AbortController() + try { + setTimeout(() => { + ac.abort() + }, 5000) + const response = await fetch('https://' + constant.apexdomain + '/api/detail/' + instance, { + headers: { 'User-Agent': constant.agent }, + agent: new https.Agent({ + rejectUnauthorized: false, + keepAlive: false + }), + signal: ac.signal + }), + json = await response.json() + if (json && json.blocks && Array.isArray(json.blocks) && json.blocks.length > 0) { + const ac2 = new AbortController() + setTimeout(() => { + ac2.abort() + }, 5000) + const result = await fetch('https://' + constant.apexdomain + '/api/list/' + instance, { + headers: { 'User-Agent': constant.agent }, + agent: new https.Agent({ + rejectUnauthorized: false, + keepAlive: false + }), + signal: ac2.signal + }), + res = await result.json() + if (res && res.instances && Array.isArray(res.instances)) { + res.instances.map(async r => { + if (r.domain === instance) { + await util.sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks\n' + + (r.api.title ? '\n' + + r.api.title + ' - ' + r.api.uri + '\n' + + (r.api.email ? 'Email: ' + r.api.email + '\n' : '') + + 'Registration: ' + (r.api.registrations ? 'open' : 'closed') + ' - Version: ' + r.api.version + '\n' + + (r.api.stats ? 'Users: ' + r.api.stats.user_count + ' - Statuses: ' + r.api.stats.status_count + ' - Domains: ' + r.api.stats.domain_count + '\n' : '') + + (r.api.description ? 'Description: ' + r.api.description + '\n' : '') : '') + + 'https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id) + } + }) + } else { + await util.sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' has ' + json.blocks.length + ' blocks - https://' + constant.apexdomain + '/#' + instance, msg.actor.id, msg.object.id) + } + } else { + await util.sendFederatedMessage(constant.nick, null, 'Instance ' + instance + ' not found, next try.', msg.actor.id, msg.object.id) + } + } catch (e) { + await util.sendFederatedMessage(constant.nick, null, e.message, msg.actor.id, msg.object.id) + // console.error(e) + } + } else { + await util.sendFederatedMessage(constant.nick, 'Hi ' + name, 'I know ' + (await client.count({ index: constant.index })).count + ' Federated Instances\nScanning ' + app.locals.server + ' instance with ' + app.locals.scantotal + ' peers\nScanned ' + app.locals.peers + ' peers from ' + app.locals.instances + ' instances, ' + app.locals.created + ' created, ' + app.locals.updated + ' updated\nhttps://' + constant.apexdomain, msg.actor.id, msg.object.id) + } + } + console.log(`New note from ${name} to ${msg.recipient.id}: ${content.join(' ')}`) + } else if (type !== 'delete') { + console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`) + } else { + console.log(`New ${msg.activity.type} from ${msg.actor.id} to ${msg.recipient.id}`) + } + } else { + console.log(JSON.stringify(msg, '', 2)) + } + }) +} diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..5c808a2 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,53 @@ +module.exports = (app, apex) => { + const constant = require('./constant'), + { readFile, writeFile } = require('fs') + app.use('/api/scan', (req, res) => { + res.header('Content-Type', 'text/event-stream') + res.header('Connection', 'keep-alive') + res.header('Cache-Control', 'no-cache') + res.header('X-Accel-Buffering', 'no') + res.write(Buffer.from('data: ...\n\n')) + req.app.locals.scan.on('data', data => { + res.write(Buffer.from(data)) + }) + res.setTimeout(20000, () => { + res.write(Buffer.from('data: ...\n\n')) + }) + }) + app.use('/api/served', (req, res, next) => { + readFile(__dirname + '/../served.txt', async (err, data) => { + if (err) { + next(err) + } else { + const num = parseInt(data.toString()) + 1 + writeFile(__dirname + '/../served.txt', num.toString(), 'utf8', (err) => { + if (err) { + next(err) + } else { + res.json({ served: num, lastscan: req.app.locals.scantotal, server: req.app.locals.server, instances: req.app.locals.instances, peers: req.app.locals.peers, created: req.app.locals.created, updated: req.app.locals.updated }) + } + }) + } + }) + }) + app.use('/api/outbox', async (req, res) => { + const outbox = await apex.getOutbox(await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true), 'true'), + notes = outbox.orderedItems.filter(e => e.object + && e.object.length > 0 + && e.object[0].type === 'Note' + && e.object[0].content + && e.object[0].content[0].startsWith('

You')).slice(0, 20).map(e => ({ + content: e.object[0].content[0], + published: e.published[0] + })) + res.json(notes) + }) + app.use('/api/inbox', async (req, res) => { + const actor = await apex.store.getObject(apex.utils.usernameToIRI(constant.nick), true) + actor._local = {} + actor._local.blockList = [] + const inbox = await apex.getInbox(actor, 'true'), + notes = inbox.orderedItems.filter(e => e.object && e.object.length > 0 && e.object[0].type === 'Note').map(e => e.object[0].content[0]) + res.json(notes) + }) +} diff --git a/lib/apiswagger.js b/lib/apiswagger.js new file mode 100644 index 0000000..0675f3f --- /dev/null +++ b/lib/apiswagger.js @@ -0,0 +1,489 @@ +const nodeinfo = require('activitypub-express/pub/nodeinfo') + +module.exports = (app, client) => { + const constant = require('./constant'), + clean = str => { + return str.replace(/[\/\\^$+?()`'¡¿¨!"·%&=;,\|\[\]{}]+/gmi, '') + } + /** + * @swagger + * /api/stats: + * get: + * summary: Retrieve stats of instances. + * description: Retrieve stats of total instances. + * responses: + * 200: + * description: A object with stats parameters. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * instance_count: + * type: integer + * description: Count of instances. + * example: 0 + * status_avg: + * type: float + * description: Average statuses of total instances. + * example: 0 + * status_max: + * type: integer + * description: Max of total statuses instances. + * example: 0 + * user_avg: + * type: float + * description: Average users of total instances. + * example: 0 + * user_max: + * type: integer + * description: Max of total users instances. + * example: 0 + * domain_avg: + * type: float + * description: Number of average domains instances. + * example: 0 + * domain_max: + * type: integer + * description: Number of max domains instances. + * example: 0 + * stats_filtered: + * type: integer + * description: Number of instances stats. + * example: 0 + */ + app.use('/api/stats', async (req, res) => { + const result = await client.search({ + index: constant.index, + body: { + size: 0, + aggs: { + instance_count: { + value_count: { + field: '_id' + } + }, + stats_filtered: { + filter: { + exists: { + field: 'api.stats' + } + } + }, + user_avg: { + avg: { + field: 'api.stats.user_count' + } + }, + user_max: { + max: { + field: 'api.stats.user_count' + } + }, + domain_avg: { + avg: { + field: 'api.stats.domain_count' + } + }, + domain_max: { + max: { + field: 'api.stats.domain_count' + } + }, + status_avg: { + avg: { + field: 'api.stats.status_count' + } + }, + status_max: { + max: { + field: 'api.stats.status_count' + } + } + + } + } + }) + res.json(Object.keys(result.aggregations).reduce((prev, curr) => ({ + ...prev, [curr]: result.aggregations[curr][Object.keys(result.aggregations[curr])[0]] + }), {})) + }) + /** + * @swagger + * /api/count: + * get: + * summary: Retrieve count of instances. + * description: Retrieve a number of total instances. + * responses: + * 200: + * description: A object with count parameter. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * count: + * type: integer + * description: Number of instances. + * example: 0 + */ + app.use('/api/count', async (req, res) => { + res.json({ count: (await client.count({ index: constant.index })).count }) + }) + /** + * @swagger + * /api/ranking: + * get: + * summary: Retrieve one ranking of instances. + * description: Retrieve a top ten ranking of total fediblock instances. + * responses: + * 200: + * description: A object with count parameter. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * domain: + * type: string + * description: Domain of the instance. + * example: "mastodon.social" + * count: + * type: integer + * description: Number of fediblocks. + * example: 0 + */ + app.use('/api/ranking', async (req, res) => { + const result = await client.search({ + index: constant.index, + body: { + size: 0, + aggs: { + blocks: { + nested: { + path: 'blocks' + }, + aggs: { + ranking: { + terms: { + field: 'blocks.domain', + size: 100 + } + } + } + } + } + } + }) + const ranking = result.aggregations.blocks.ranking.buckets + res.json(ranking.map(r => ({ domain: r.key, count: r.doc_count }))) + }) + /** + * @swagger + * /api/list/{instance}: + * get: + * summary: Search result array of intances. + * description: Retrieve a result array of instances matching search input. + * parameters: + * - in: path + * name: instance + * required: true + * description: String for search instance + * schema: + * type: string + * responses: + * 200: + * description: A list of instances. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * instances: + * type: array + * items: + * type: data + * description: List of instances. + * example: "mastodon.social" + * suggests: + * type: array + * items: + * type: data + * description: Suggest of the instance. + * example: "mastodon.social" + */ + app.use('/api/list/:instance', async (req, res) => { + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 10, + query: { + wildcard: { + instance: { + value: `*${clean(req.params.instance)}*` + } + }, + }, + suggest: { + suggests: { + text: req.params.instance, + term: { + field: 'instance', + size: 3, + sort: 'score', + suggest_mode: 'always', + max_edits: 2, + min_word_length: 1 + } + } + } + }) + const instances = result.hits && result.hits.hits && result.hits.hits.length > 0 ? result.hits.hits : [], + suggests = result.suggest.suggests && result.suggest.suggests.length > 0 && result.suggest.suggests[0].options.length > 0 ? result.suggest.suggests[0].options : [] + res.json({ + instances: instances.map(instance => ({ + domain: instance._source.instance, + api: instance._source.api ? instance._source.api : null, + blocks: instance._source.blocks && instance._source.blocks.length > 0 ? instance._source.blocks.length : null, + last: instance._source.last ? instance._source.last : null, + nodeinfo: instance._source.nodeinfo ? true : false + })), suggests: suggests.map(instance => instance.text) + }) + } else { + res.status(404).end() + } + }) + /** + * @swagger + * /api/detail/{instance}: + * get: + * summary: Search result array of fediblocks intances. + * description: Retrieve a result array of fediblock instances matching search input. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to detail of the instance + * schema: + * type: string + * responses: + * 200: + * description: A list of instances. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: array + * items: + * type: object + * properties: + * domain: + * type: string + * description: Domain of the block instance. + * example: "mastodon.social" + * comment: + * type: string + * description: Comment of the block instance. + * example: "mastodon.social" + * severity: + * type: string + * description: Severity of the block instance. + * example: "mastodon.social" + * + */ + app.use('/api/detail/:instance', async (req, res) => { + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: clean(req.params.instance) + } + } + }) + const instances = result.hits && result.hits.hits && result.hits.hits.length > 0 ? result.hits.hits : [] + res.json(instances.length > 0 && instances[0]._source.blocks && instances[0]._source.blocks.length > 0 ? + { + blocks: instances[0]._source.blocks.map(instance => ({ domain: instance.domain, comment: instance.comment, severity: instance.severity })), + last: instances[0]._source.last, + instance: instances[0]._source.instance, + nodeinfo: instances[0]._source.nodeinfo, + api: instances[0]._source.api, + took: result.took + } : []) + } else { + res.status(404).end() + } + }) + /** + * @swagger + * /api/detail_api/{instance}: + * get: + * summary: Search result of detail's api intance. + * description: Retrieve a result of detail's api instance. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to detail of the instance + * schema: + * type: string + * responses: + * 200: + * description: Detail of the api instance. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + */ + app.use('/api/detail_api/:instance', async (req, res) => { + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: clean(req.params.instance) + } + } + }) + const instances = result.hits && result.hits.hits && result.hits.hits.length > 0 ? result.hits.hits : [] + res.json(instances.length > 0 ? instances[0]._source.api : {}) + } else { + res.status(404).end() + } + }) + /** + * @swagger + * /api/detail_nodeinfo/{instance}: + * get: + * summary: Search result of detail's nodeinfo intance. + * description: Retrieve a result of detail's nodeinfo instance. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to detail of the instance + * schema: + * type: string + * responses: + * 200: + * description: Detail of the nodeinfo instance. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + */ + app.use('/api/detail_nodeinfo/:instance', async (req, res) => { + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: clean(req.params.instance) + } + } + }) + const instances = result.hits && result.hits.hits && result.hits.hits.length > 0 ? result.hits.hits : [] + res.json(instances.length > 0 ? instances[0]._source.nodeinfo : {}) + } else { + res.status(404).end() + } + }) + /** + * @swagger + * /api/block_count/{instance}: + * get: + * summary: Retrieve count of fediblocked instances. + * description: Retrieve a number of total fediblocked instances. + * parameters: + * - in: path + * name: instance + * required: true + * description: String to search fediblocks of the instance + * schema: + * type: string + * responses: + * 200: + * description: A object with block_count parameter. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * block_count: + * type: integer + * description: Number of fediblock instances. + * example: 0 + * instances: + * type: array + * items: + * type: object + * properties: + * instance: + * type: string + * description: Instance of the block. + * example: mastodon.social + * comment: + * type: string + * description: Comment about the block. + * example: spam + */ + app.use('/api/block_count/:instance', async (req, res) => { + if (req.params.instance && req.params.instance.length > 0) { + const result = await client.search({ + index: constant.index, + size: 9999, + query: { + nested: { + path: 'blocks', + query: { + term: { + 'blocks.domain': clean(req.params.instance) + } + } + } + } + }) + const instances = result.hits && result.hits.hits && result.hits.hits.length > 0 ? result.hits.hits : [], + instancescomment = instances.map(i => ({ + instance: i._source.instance, comment: i._source.blocks.find(block => block.domain === clean(req.params.instance)).comment + })) + global.gc() + res.json({ + block_count: instances.length, + instances: instancescomment, + took: result.took + }) + } else { + res.status(404).end() + } + }) +} diff --git a/lib/constant.js b/lib/constant.js new file mode 100644 index 0000000..8e724e8 --- /dev/null +++ b/lib/constant.js @@ -0,0 +1,14 @@ +module.exports = { + index: 'fediblock', // elasticsearch index name + nick: 'fediblockbot', // nick of bot + icon: 'https://manalejandro.com/favicon.png', // icon of the instance + dburl: 'mongodb://fediblock-mongodb:27017', // mongodb connection + dbname: 'fediblock', // mongodb database name + apexdomain: 'fediblock.manalejandro.com', // domain of the instance + agent: 'fediblock.manalejandro.com', // agent of fetch requests + elasticnode: 'http://fediblock-elasticsearch:9200', // elasticsearch connection + workers: 3, // async concurrent workers to scan + schedule: '5,11,17,23', // UTC hours to publish bot followers federated stats + filterdomains: ['activitypub-troll.cf'], // domains filtered to scan + initialscan: 'mastodon.social' // initial federated domain to scan +} diff --git a/lib/fediblock.js b/lib/fediblock.js new file mode 100644 index 0000000..16bcaad --- /dev/null +++ b/lib/fediblock.js @@ -0,0 +1,261 @@ +let servers +module.exports = async (client, apex, app) => { + const util = require('./util')(apex), + constant = require('./constant'), + https = require('https'), + schedule = require('node-schedule'), + workers = constant.workers, + getAccount = api => { + if (api && api.contact_account.acct) { + const acct = api.contact_account.acct.split('@') + if (acct.length > 1) { + return `https://${acct[1]}/users/${acct[0]}` + } else { + return `https://${api.uri.replace(/https?:\/\//, '')}/users/${acct.join('')}` + } + } else { + return '' + } + }, + requestPart = async uri => { + const ac = new AbortController() + try { + setTimeout(() => { + ac.abort() + }, 5000) + const response = await fetch(uri, { + headers: { 'User-Agent': constant.agent }, + agent: new https.Agent({ + rejectUnauthorized: false, + keepAlive: false + }), + signal: ac.signal, + keepalive: false, + timeout: 4000 + }), + json = await response.json() + setImmediate(() => { ac.abort() }) + return json + } catch (e) { + setImmediate(() => { ac.abort() }) + // console.error(e) + return + } + }, + scanInstance = async instance => { + try { + const json = await requestPart(`https://${instance}/api/v1/instance/domain_blocks`) + if (Array.isArray(json) && json.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: instance + } + } + }), + instancelocated = result.hits && result.hits.hits ? result.hits.hits : [], + blocks = json.map(block => { + if (block.comment && block.comment.length > 8190) { + block.comment = block.comment.slice(0, 8190) + } + return block + }), + [api, nodeinfo, peers] = await Promise.all([ + requestPart(`https://${instance}/api/v1/instance`), + requestPart(`https://${instance}/nodeinfo/2.0`), + requestPart(`https://${instance}/api/v1/instance/peers`) + ]) + if (instancelocated.length === 0) { + await client.index({ + index: constant.index, + body: { + instance, + api, + nodeinfo, + blocks, + peers, + last: new Date() + } + }) + app.locals.created++ + return await util.sendFederatedMessage(constant.nick, 'New Fediblock Instance', `Fediblock Instance ${instance} with ${json.length} blocks - https://${constant.apexdomain}#${instance}`, getAccount(api)) + } else { + const elasticinstance = instancelocated[0]._source.blocks || [] + if (Array.isArray(elasticinstance)) { + if (json.length !== elasticinstance.length + || (instancelocated[0]._source.last && instancelocated[0]._source.last < new Date(Date.now() - 2678400000)) + || !instancelocated[0]._source.api + || !instancelocated[0]._source.nodeinfo + || !instancelocated[0]._source.peers) { + await client.update({ + index: constant.index, + id: instancelocated[0]._id, + doc: { + api: api ? api : instancelocated[0]._source.api, + nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : elasticinstance, + peers: peers && peers.length > 0 ? peers : instancelocated[0]._source.peers, + last: new Date() + }, + doc_as_upsert: true + }) + app.locals.updated++ + if (instancelocated[0]._source.api && instancelocated[0]._source.api.uri && instancelocated[0]._source.api.contact_account && instancelocated[0]._source.api.contact_account.acct) { + const difference = blocks.filter(block => block.domain && block.domain.trim().length > 0 && !elasticinstance.some(instance => block.domain === instance.domain)) + if (difference.length > 0 && difference.length < 50) { + return await util.sendFederatedMessage(constant.nick, 'Detected #Fediblock by Fediblock Instance', `You blocked new instances: ${difference.map(d => d.domain).join(', ')} - https://${constant.apexdomain}#${instance}`, getAccount(instancelocated[0]._source.api)) + } else { + return + } + } else { + return + } + } else { + return + } + } else { + return + } + } + } else { + return + } + } catch (e) { + console.error(e) + return + } + }, + scanPart = async (instancesall, index) => { + for (const instance of instancesall) { + try { + app.locals.scan.emit('data', 'data: ' + (app.locals.scannum > 0 ? '(' + app.locals.scannum-- + ':' + (index + 1) + '): ' + instance : ': ' + instance) + '\n\n') + app.locals.peers++ + await scanInstance(instance) + } catch (e) { + console.error(e) + } + } + global.gc() + return + }, + scanIndex = async (server, instancesall) => { + try { + const [api, nodeinfo, blocks] = await Promise.all([ + requestPart(`https://${server}/api/v1/instance`), + requestPart(`https://${server}/nodeinfo/2.0`), + requestPart(`https://${server}/api/v1/instance/domain_blocks`) + ]) + if (api && typeof api === 'object' && api.version) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: server + } + } + }), + instancelocated = result.hits && result.hits.hits ? result.hits.hits : [] + if (instancelocated.length === 0) { + await client.index({ + index: constant.index, + body: { + instance: server, + peers: instancesall, + api, + nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : [], + last: new Date() + } + }) + app.locals.created++ + } else { + const elasticinstance = instancelocated[0]._source.peers || [] + if (Array.isArray(elasticinstance) && Array.isArray(instancesall) && instancesall.length > 0) { + if (instancesall.length !== elasticinstance.filter(i => !constant.filterdomains.some(d => i.endsWith(d))).length) { + await client.update({ + index: constant.index, + id: instancelocated[0]._id, + doc: { + peers: instancesall.length > 0 ? instancesall : elasticinstance, + api: api ? api : instancelocated[0]._source.api, + nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : instancelocated[0]._source.blocks, + last: new Date() + }, + doc_as_upsert: true + }) + app.locals.updated++ + } else { + return + } + } else { + return + } + } + } else { + return + } + } catch (e) { + console.error(e) + return + } + }, + scanReturn = async () => { + global.gc() + app.locals.scannum = 0 + if (servers && servers.length > 0) { + return await scan(servers.shift()) + } else { + return await scan(constant.initialscan) + } + }, + scan = async server => { + if (server) { + try { + console.log(server) + app.locals.scan.emit('data', 'data: ' + server + ' peers...\n\n') + const instances = await requestPart(`https://${server}/api/v1/instance/peers`), + instancesall = Array.isArray(instances) && instances.length > 0 + ? instances.filter(i => !constant.filterdomains.some(d => i.endsWith(d))) + : [], + instancessorted = instancesall.sort((a, b) => a < b ? -1 : a > b ? 1 : 0), + parts = [], + split = workers, + chunkSize = Math.floor(instancessorted.length / split) + await scanIndex(server, instancesall) + if (instancesall.length > 0) { + if (!servers || servers.length === 0) { + servers = instancesall.sort(() => Math.random() - 0.5) + } + if (server && server === constant.initialscan) { + return await scan(servers.shift()) + } + app.locals.scannum = instancessorted.length + app.locals.scantotal = instancessorted.length + app.locals.server = server + app.locals.instances++ + for (let i = 0; i < instancessorted.length; i += chunkSize) { + const chunk = instancessorted.slice(i, i + chunkSize) + parts.push(chunk) + } + await Promise.all(parts.map(async (p, i) => await scanPart(p, i))) + return await scanReturn() + } else { + return await scanReturn() + } + } catch (e) { + console.error(e) + return await scanReturn() + } + } else { + return await scanReturn() + } + }, + job = schedule.scheduleJob('0 ' + constant.schedule + ' * * *', async () => { + return await util.sendFederatedMessage(constant.nick, null, 'Scanning ' + app.locals.server + ' instance with ' + app.locals.scantotal + ' peers\nScanned ' + app.locals.peers + ' peers from ' + app.locals.instances + ' instances, ' + app.locals.created + ' created, ' + app.locals.updated + ' updated\nhttps://' + constant.apexdomain) + }) + return await scanReturn() +} diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..df98789 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,7 @@ +module.exports = app => { + const morgan = require('morgan'), + rfs = require('rotating-file-stream'), + accessLogStream = rfs.createStream('access.log', { interval: '1d', path: __dirname + '/../logs' }), + logger = morgan('combined', { stream: accessLogStream }) + app.use(logger) +} diff --git a/lib/swagger.js b/lib/swagger.js new file mode 100644 index 0000000..85218a3 --- /dev/null +++ b/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/lib/util.js b/lib/util.js new file mode 100644 index 0000000..1fe321c --- /dev/null +++ b/lib/util.js @@ -0,0 +1,45 @@ +module.exports = apex => { +const urlToId = url => { + try { + if (typeof new URL(url) === 'object' && url.split('/').length === 5 && url.split('/')[3].length > 0) { + return '@' + url.split('/')[4] + '@' + url.split('/')[2] + } else { + return url + } + } catch (e) { + return url + } +}, + sendFederatedMessage = async (id, summary, message, recipient, reply) => { + const users = message.split(' ').filter(token => token.startsWith('@') && token.split('@').length === 3) + .map(token => `https://${token.split('@')[2]}/users/${token.split('@')[1]}`), + mentions = message.split(' ').filter(token => token.startsWith('@') && token.split('@').length === 3) + .map(token => { return { type: 'Mention', href: `https://${token.split('@')[2]}/users/${token.split('@')[1]}`, name: `@${token.split('@')[1]}` } }), + hashtags = message.split(' ').filter(token => token.startsWith('#')) + .map(token => { return { id: `https://mastodon.social/tags/${token.split('#')[1]}`, name: token } }), + images = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(jpg|png|gif|jpeg|ppm)$/i)) + .map(token => { return { type: 'Image', url: token } }), + audio = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(mp3|wav|flac)$/i)) + .map(token => { return { type: 'Audio', url: token } }), + video = message.split(' ').filter(token => token.match(/^https?:\/\//i) && token.match(/\.(mp4|flv|avi)$/i)) + .map(token => { return { type: 'Video', url: token } }), + links = message.split(' ').filter(token => token.match(/^https?:\/\//i) && !images.concat(audio).concat(video) + .some(link => link.url === token)).map(token => { return { type: 'Link', href: token } }), + allusers = users.concat([recipient ? recipient : (await apex.store.getObject(apex.utils.usernameToIRI(id), true)).followers[0]]), + act = await apex.buildActivity('Create', apex.utils.usernameToIRI(id), ['https://www.w3.org/ns/activitystreams#Public'].concat(allusers), { + object: { + type: 'Note', + name: `Fediblock Instance`, + summary: summary ? summary : null, + content: `

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

`, + tag: hashtags.concat(mentions), + attachment: images.concat(links).concat(audio).concat(video), + inReplyTo: reply ? reply : null + } + }) + act.object[0].id = act.id + await apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(id), true), act) + } + return { urlToId, sendFederatedMessage } +} diff --git a/lib/worker-fediblock.js b/lib/worker-fediblock.js new file mode 100644 index 0000000..65b184e --- /dev/null +++ b/lib/worker-fediblock.js @@ -0,0 +1,106 @@ +module.exports = async (client, apex, app) => { + util = require('./util')(apex), + constant = require('./constant'), + { parentPort } = require('worker_threads'), + scanInstance = async instance => { + try { + const json = await util.requestPart(`https://${instance}/api/v1/instance/domain_blocks`) + if (Array.isArray(json) && json.length > 0) { + const result = await client.search({ + index: constant.index, + size: 1, + query: { + term: { + instance: instance + } + } + }), + instancelocated = result.hits && result.hits.hits ? result.hits.hits : [], + blocks = json.map(block => { + if (block.comment && block.comment.length > 8190) { + block.comment = block.comment.slice(0, 8190) + } + return block + }), + [api, nodeinfo, peers] = await Promise.all([ + util.requestPart(`https://${instance}/api/v1/instance`), + util.requestPart(`https://${instance}/nodeinfo/2.0`), + util.requestPart(`https://${instance}/api/v1/instance/peers`) + ]) + if (instancelocated.length === 0) { + await client.index({ + index: constant.index, + body: { + instance, + api, + nodeinfo, + blocks, + peers, + last: new Date() + } + }) + app.locals.created++ + return await util.sendFederatedMessage(constant.nick, 'New Fediblock Instance', `Fediblock Instance ${instance} with ${json.length} blocks - https://${constant.apexdomain}#${instance}`, util.getAccount(api)) + } else { + const elasticinstance = instancelocated[0]._source.blocks || [] + if (Array.isArray(elasticinstance)) { + if (json.length !== elasticinstance.length + || (instancelocated[0]._source.last && instancelocated[0]._source.last < new Date(Date.now() - 2678400000)) + || !instancelocated[0]._source.api + || !instancelocated[0]._source.nodeinfo + || !instancelocated[0]._source.peers) { + await client.update({ + index: constant.index, + id: instancelocated[0]._id, + doc: { + api: api ? api : instancelocated[0]._source.api, + nodeinfo: nodeinfo ? nodeinfo : instancelocated[0]._source.nodeinfo, + blocks: blocks && blocks.length > 0 ? blocks : elasticinstance, + peers: peers && peers.length > 0 ? peers : instancelocated[0]._source.peers, + last: new Date() + }, + doc_as_upsert: true + }) + app.locals.updated++ + if (instancelocated[0]._source.api && instancelocated[0]._source.api.uri && instancelocated[0]._source.api.contact_account && instancelocated[0]._source.api.contact_account.acct) { + const difference = blocks.filter(block => block.domain && block.domain.trim().length > 0 && !elasticinstance.some(instance => block.domain === instance.domain)) + if (difference.length > 0 && difference.length < 50) { + return await util.sendFederatedMessage(constant.nick, 'Detected #Fediblock by Fediblock Instance', `You blocked new instances: ${difference.map(d => d.domain).join(', ')} - https://${constant.apexdomain}#${instance}`, util.getAccount(instancelocated[0]._source.api)) + } else { + return + } + } else { + return + } + } else { + return + } + } else { + return + } + } + } else { + return + } + } catch (e) { + console.error(e) + return + } + }, + scanPart = async (instancesall, index) => { + for (const instance of instancesall) { + try { + app.locals.scan.emit('data', 'data: ' + (app.locals.scannum > 0 ? '(' + app.locals.scannum-- + ':' + (index + 1) + '): ' + instance : ': ' + instance) + '\n\n') + app.locals.peers++ + await scanInstance(instance) + } catch (e) { + console.error(e) + } + } + return + }, + work = (part, index) => { + return parentPort.postMessage({ func: scanPart, args: [part, index] }) + } + +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4f961af --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "fediblock-instance", + "version": "1.0.0", + "description": "Fediblock Instance", + "author": "ale", + "repository": { + "type": "git", + "url": "https://gitlab.com/manalejandro/fediblock-instance" + }, + "license": "MIT", + "scripts": { + "start": "node --max-old-space-size=1024 --expose-gc server.js", + "build": "rm -rf dist/* && parcel build public/index.html --no-source-maps --dist-dir dist/", + "install": "cd node_modules && rm -rf http-signature && rm -rf request/node_modules/http-signature && mv @peertube/http-signature ." + }, + "dependencies": { + "@elastic/elasticsearch": "^8.14.0", + "@peertube/http-signature": "^1.7.0", + "activitypub-express": "^4.4.2", + "dayjs": "^1.11.12", + "express": "^4.19.2", + "html2canvas": "^1.4.1", + "mongodb": "^4.17.2", + "morgan": "^1.10.0", + "node-schedule": "^2.1.1", + "parcel": "^2.12.0", + "rotating-file-stream": "^3.2.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + } +} diff --git a/public/favicon.ico b/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/public/index.html b/public/index.html new file mode 100644 index 0000000..10da592 --- /dev/null +++ b/public/index.html @@ -0,0 +1,77 @@ + + + + + + + Fediblock Instance Φ + + + + + + + + + + + + + + + + +

Fediblock Instance Φ

+

+

 

+

+

Search in public instances - + API docs - Git

+
+ + +
+
+
+
+
+
+
    +
    +
    + +
    +
    +
    Served 0 times - Last scan 0 peers of
    + Total scanned 0 instances with 0 peers - 0 created - 0 + updated
    + by ale ©2024 - matrix off
    + + + + \ No newline at end of file diff --git a/public/loaders.css b/public/loaders.css new file mode 100644 index 0000000..0bab6ce --- /dev/null +++ b/public/loaders.css @@ -0,0 +1,413 @@ +/* HTML:
    */ +.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:
    */ +.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:
    */ +.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:
    */ +.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:
    */ +.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:
    */ +.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:
    */ +.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/public/main.css b/public/main.css new file mode 100644 index 0000000..ee260c6 --- /dev/null +++ b/public/main.css @@ -0,0 +1,378 @@ +body { + background-color: #212529; + text-align: center; + font-family: Verdana, Geneva, Tahoma, sans-serif; + scrollbar-width: thin; +} + +h1, +h3, +h4, +#scan { + margin: 0 auto; + color: #fefefe; + text-align: center; +} + +h3 { + padding: 10px 0; + border-bottom: 1px solid #ccc; + margin-bottom: 1rem; +} + +a, +a:hover { + color: #fefefe; + text-decoration: none; +} + +input[type="text"] { + padding: 10px; + border: none; + border-bottom: 1px solid #ccc; + background-color: #262c33; + color: #fefefe; +} + +#placeholder { + font-size: small; + margin: 0 auto; + color: #fefefe; +} + +hr { + margin: 1rem auto; + border-bottom: 1px solid #ccc; + width: 50%; +} + +#tooltip { + visibility: hidden; + font-size: small; + text-align: left; + width: 2.2in; + background-color: #212529; + color: #fefefe; + border-radius: 5px; + padding: 1rem 1rem 0 0; + position: absolute; + z-index: 1; + opacity: 0; + transition: 0.5s; + border: 1px solid #ccc; +} + +#count:hover #tooltip { + visibility: visible; + opacity: 1; +} + +#reverse { + margin: 0.5rem; + padding: 0.5rem; + border: 1px solid #ccc; + background-color: #fefefe; + color: #262c33; +} + +#reverse:hover { + color: #fefefe; + background-color: #262c33; + cursor: pointer; +} + +#instancelist { + list-style-type: none; + margin: 20px 0; + padding: 0; + color: #fefefe; + max-height: 58vh; + overflow: auto; + scrollbar-width: thin; +} + +@media (min-width: 992px) { + #instancelist li { + width: 28%; + } + + #instancelist li:hover { + width: 33%; + } + + #instance, + #placeholder, + h4 { + width: 30%; + } +} + +@media (min-width: 600px) and (max-width: 992px) { + #instancelist li { + width: 60%; + } + + #instancelist li:hover { + width: 65%; + } + + #instance, + #placeholder, + h4 { + width: 63%; + } +} + +@media (max-width: 600px) { + #instancelist li { + width: 80%; + } + + #instancelist li:hover { + width: 85%; + } + + #instance, + #placeholder, + h4 { + width: 83%; + } + + #apigit { + display: none; + } +} + +#instancelist li { + color: #fefefe; + padding: 10px; + border-bottom: 1px solid #ccc; + margin: 0 auto; + max-height: 1.5em; + overflow: hidden; + min-height: 1.5em; + line-height: 1.5; +} + +#instancelist li:hover { + background-color: #333; + cursor: pointer; + max-height: fit-content; +} + +#instancelist li img { + width: 70%; + margin: 0 auto; +} + +@keyframes opacity { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +footer { + text-align: center; + font-size: 12px; + padding: 10px; + color: #ccc; +} + +#count { + text-decoration: underline dotted #fefefe; + cursor: help; + position: relative; + display: inline-block; + text-underline-offset: 2px; +} + +#blocklist { + width: 90%; + text-align: left; + list-style: none; + padding: 1rem; +} + +#blocklist li { + color: #262c33; + white-space: nowrap; + word-wrap: break-word; + overflow: hidden; +} + +#blocklist li:hover { + white-space: inherit; + cursor: pointer; + color: #212529; +} + +#blocklist li a, +#blocklist li a:hover, +#blocklist li a:visited { + color: #262c33; + text-decoration: underline; +} + +#blockinstance a, +#blockinstance a:hover { + color: #262c33; + text-decoration: underline; + cursor: help; +} + +#blockcount { + font-weight: bolder; +} + +#modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + scrollbar-width: thin; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + position: relative; + background-color: #fefefe; + margin: 10px auto; + padding: 0; + border: 1px solid #888; + width: 25%; + -webkit-animation-name: animatetop; + -webkit-animation-duration: 0.4s; + animation-name: animatetop; + animation-duration: 0.4s; +} + +@media only screen and (max-width: 600px) { + .modal-content { + width: 90%; + } +} + +.modal-header { + padding: 6px; + background-color: #212529; + color: #fefefe; +} + +.modal-body { + padding: 6px; +} + +.modal-footer { + padding: 6px; + background-color: #212529; + color: #fefefe; +} + +#closemodal, +#download, +#capture { + color: #aaa; + float: right; + font-size: 3rem; + font-weight: bolder; + margin: 0px 15px; +} + +#closemodal:hover, +#closemodal:focus, +#download:hover, +#download:focus, +#capture:hover, +#capture:focus { + color: #fefefe; + text-decoration: none; + cursor: pointer; +} + +@keyframes animatetop { + from { + top: -300px; + opacity: 0; + } + + to { + top: 0; + opacity: 1; + } +} + +@keyframes animatebottom { + from { + top: 0; + opacity: 1; + } + + to { + top: -300px; + opacity: 0; + } +} + +h4 { + white-space: nowrap; + overflow: hidden; + position: relative; + border-left: 1px solid #fefefe; + border-right: 1px solid #fefefe; + transition: 0.2s; +} + +h4 p { + margin: 0.3rem 0; +} + +h4:hover { + width: 90%; +} + +#bounce { + display: inline-block; + animation: marquee 90s linear infinite; +} + +#bounce:hover { + animation-play-state: paused; +} + +@keyframes marquee { + 0% { + transform: translateX(100vw); + } + + 100% { + transform: translateX(-100%); + } +} + +#loader-content { + z-index: 1; + background-color: #0006; + width: 100%; + height: 100%; + display: none; + position: fixed; + top: 0; + left: 0; + overflow: auto; + scrollbar-width: thin; +} + +#loader { + background-color: #fefefe; + width: fit-content; + margin: 50vh auto; + z-index: 2; +} \ No newline at end of file diff --git a/public/main.js b/public/main.js new file mode 100644 index 0000000..6a1537b --- /dev/null +++ b/public/main.js @@ -0,0 +1,418 @@ +document.addEventListener('DOMContentLoaded', function () { + loading() + var hold = false, + external = false, + timeout = undefined, + ac = undefined, + last = undefined + document.getElementById('instance').addEventListener('keydown', function (event) { + if (event.key && ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && event.target.value !== ''))) { + last = event.target.value + } else if (event.key === 'Backspace' && event.target.value === '') { + last = '' + } else { + event.preventDefault() + event.stopPropagation() + } + }) + document.getElementById('instance').addEventListener('keyup', function (event) { + if (event.key && ((event.key.length === 1 && /[a-z0-9.\-*:]/i.test(event.key)) || (event.key === 'Backspace' && last !== ''))) { + keypress(event, event.target.value) + } else { + event.preventDefault() + event.stopPropagation() + } + }) + var modal = document.getElementById('modal'), + closemodal = document.getElementById('closemodal'), + modalcontent = document.querySelector('.modal-content') + document.getElementById('reverse').addEventListener('click', function (event) { + var content = document.getElementById('instance').value + if (content && content.length > 0) { + loading() + fetch('/api/block_count/' + content).then(async function (result) { + var res = await result.json() + if (res && res.block_count >= 0) { + document.getElementById('blockcount').innerText = res.block_count + document.getElementById('blockinstance').innerText = 'ing ' + content + document.getElementById('blocktook').innerText = res.took + var list = document.getElementById('blocklist'), + download = document.getElementById('download') + download.removeAttribute('href') + download.removeAttribute('download') + download.style.display = 'none' + while (list.hasChildNodes()) { + list.removeChild(list.firstChild) + } + res.instances.map((instance, index) => { + var li = document.createElement('li'), + text = document.createTextNode((index + 1) + '. '), + link = '' + instance.instance + '', + text2 = document.createTextNode(instance.comment ? ' - ' + instance.comment : '') + li.appendChild(text) + li.insertAdjacentHTML('beforeend', link) + li.appendChild(text2) + blocklist.appendChild(li) + }) + listinstance(content, new AbortController()) + modalcontent.style.animationName = 'animatetop' + modal.style.display = 'block' + var locationsearch = !new URLSearchParams(window.location.search).has('reverse') ? window.location.search ? window.location.search + '&reverse' : '?reverse' : window.location.search + history.pushState({}, null, '/' + locationsearch + '#' + content) + if (new URLSearchParams(window.location.search).has('matrix')) { + var walker = document.createTreeWalker(list, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + document.getElementById('loader-content').style.display = 'none' + } + }) + } + }) + document.body.addEventListener('click', function (event) { + if (event.target == modal) { + modalcontent.style.animationName = 'animatebottom' + } + }) + closemodal.addEventListener('click', function (event) { + modalcontent.style.animationName = 'animatebottom' + }) + modalcontent.addEventListener('animationend', function (event) { + if (event.animationName === 'animatebottom') { + modal.style.display = 'none' + } + }) + document.getElementById('title').addEventListener('click', function (event) { + document.getElementById('instance').value = '' + if (new URLSearchParams(window.location.search).has('matrix')) { + window.location.href = '/?matrix' + } else { + window.location.href = '/' + } + event.preventDefault() + event.stopPropagation() + }) + document.getElementById('capture').addEventListener('click', function (event) { + html2canvas(document.querySelector('.modal-body')).then(function (canvas) { + var a = document.createElement('a') + a.download = 'fediblock-' + Date.now() + '.png' + a.href = canvas.toDataURL({ type: 'image/png' }) + a.type = 'image/png' + a.target = '_blank' + a.dispatchEvent(new MouseEvent('click')) + }) + }) + function keypress(event, content) { + loading() + if (timeout) { + clearTimeout(timeout) + } + if (ac && ac.signal) { + ac.abort() + } + ac = new AbortController() + if (content.length === 0) { + hold = true + ranking() + } else if (content.length > 0 && !hold) { + hold = true + listinstance(content, ac) + } else if (content.length > 0 && hold) { + timeout = setTimeout(() => { + listinstance(content, ac) + }, 300) + } + } + function listinstance(content, ac) { + loading() + var list = document.getElementById('instancelist') + fetch('/api/list/' + content, { signal: ac.signal }).then(async function (result) { + var res = await result.json() + if (res && Array.isArray(res.instances) && Array.isArray(res.suggests)) { + while (list.hasChildNodes()) { + list.removeChild(list.firstChild) + } + res.instances.map(r => { + var li = document.createElement('li') + li.innerHTML = r.domain + (r.blocks ? ' - ' + r.blocks + ' blocks' : '') + + (r.nodeinfo ? ' ' : '') + + (r.api.title ? '

    ' + r.api.title + ' - ' + r.api.uri + '
    ' + + (r.last ? 'Last update: ' + (new Date(r.last)).toLocaleString() + '
    ' : '') + + (r.api.email ? 'Email: ' + r.api.email + '
    ' : '') + + 'Registration: ' + (r.api.registrations ? 'open' : 'closed') + ' - Version: ' + r.api.version + '
    ' + + (r.api.stats ? 'Users: ' + r.api.stats.user_count + ' - Statuses: ' + r.api.stats.status_count + ' - Domains: ' + r.api.stats.domain_count + '
    ' : '') + + (r.api.description ? 'Description: ' + r.api.description + '
    ' : '') + + (r.api.thumbnail ? '
    ' : '') : '') + li.setAttribute('data-domain', r.domain) + li.addEventListener('click', function (event) { + if (event.target.matches('a')) { + return true + } + loading() + var blocklist = document.getElementById('blocklist'), + download = document.getElementById('download'), + domain = event.target.getAttribute('data-domain') + while (blocklist.hasChildNodes()) { + blocklist.removeChild(blocklist.firstChild) + } + fetch('/api/detail/' + domain).then(async function (result) { + var res = await result.json() + if (res.blocks && Array.isArray(res.blocks) && res.blocks.length > 0) { + var csv = '#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate\n' + res.blocks.map((r, i) => { + var liblock = document.createElement('li'), + text = document.createTextNode((i + 1) + '. '), + link = '' + r.domain + '', + textSeverity = document.createTextNode(r.severity ? ' - ' + r.severity : ''), + textComment = document.createTextNode(r.comment ? ' - ' + r.comment : '') + liblock.appendChild(text) + liblock.insertAdjacentHTML('beforeend', link) + liblock.appendChild(textSeverity) + liblock.appendChild(textComment) + blocklist.appendChild(liblock) + csv += !r.domain.match(/\*/) ? r.domain + ',' + (r.severity ? r.severity : '') + ',False,False,' + (r.comment ? '"' + r.comment + '"' : '') + ',False\n' : '' + }) + modalcontent.style.animationName = 'animatetop' + modal.style.display = 'block' + document.getElementById('blockcount').innerText = res.blocks.length + document.getElementById('blockinstance').innerHTML = 'ed by ' + (res.api ? '' + + res.instance + '' : res.instance) + (res.nodeinfo ? ' ' : '') + + '
    Last update: ' + (new Date(res.last)).toLocaleString() + document.getElementById('blocktook').innerText = res.took + if (csv.split('\n').length > 2) { + download.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' })) + download.download = 'fediblock-' + res.instance + '.csv' + download.style.display = 'inherit' + } else { + download.removeAttribute('href') + download.removeAttribute('download') + download.style.display = 'none' + } + if (new URLSearchParams(window.location.search).has('matrix')) { + var walker = document.createTreeWalker(blocklist, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + } else { + var a = document.createElement('a') + a.href = '/api/detail_api/' + domain + a.title = 'API info for ' + domain + a.target = '_blank' + a.dispatchEvent(new MouseEvent('click')) + } + document.getElementById('loader-content').style.display = 'none' + }) + window.location.hash = domain + document.getElementById('loader-content').style.display = 'none' + }) + list.appendChild(li) + }) + var placeholder = document.getElementById('placeholder') + if (res.suggests.length > 0) { + placeholder.innerHTML = res.suggests.map(instance => '' + instance + '').join(', ') + } else { + placeholder.innerText = '' + } + if (external) { + document.getElementById('instancelist').childNodes.forEach(function (node) { + if (node.textContent.split(' ')[0].trim() === content) { + node.dispatchEvent(new MouseEvent('click')) + } + }) + external = false + } + if (new URLSearchParams(window.location.search).has('matrix')) { + var walker = document.createTreeWalker(list, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + } + document.getElementById('loader-content').style.display = 'none' + hold = false + }).catch(err => { + // console.error(err) + document.getElementById('loader-content').style.display = 'none' + hold = false + }) + } + function ranking() { + loading() + var list = document.getElementById('instancelist'), + placeholder = document.getElementById('placeholder') + fetch('/api/ranking').then(async function (result) { + var res = await result.json() + if (Array.isArray(res) && res.length > 0) { + while (list.hasChildNodes()) { + list.removeChild(list.firstChild) + } + var li = document.createElement('li'), + strong = document.createElement('strong'), + text = document.createTextNode('Top 100') + strong.appendChild(text) + li.appendChild(strong) + list.appendChild(li) + res.map((r, i) => { + var li = document.createElement('li'), + text = document.createTextNode(`${i + 1} - ${r.domain} - ${r.count} blocks`) + li.addEventListener('click', function (event) { + var instance = document.getElementById('instance'), + text = event.target.innerText.split(' - ')[1].trim() + instance.value = text + document.getElementById('reverse').click() + }) + li.appendChild(text) + list.appendChild(li) + }) + } + if (new URLSearchParams(window.location.search).has('matrix')) { + var walker = document.createTreeWalker(list, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + } + placeholder.innerText = '' + hold = false + document.getElementById('loader-content').style.display = 'none' + }).catch(err => { + console.error(err) + hold = false + document.getElementById('loader-content').style.display = 'none' + }) + } + function suggest(urlitem) { + var instance = document.getElementById('instance') + window.location.hash = urlitem + instance.value = urlitem + listinstance(urlitem, new AbortController()) + } + function loading() { + var loader = document.getElementById('load'), + loaders = ['loader-pong', 'loader-pacman', 'loader-abyss', 'loader-jump', 'loader-loading', 'loader-avenger', 'loader-mario'] + if (loader.classList.value) { + loader.classList.remove(loader.classList.value) + } + loader.classList.add(loaders[Math.floor(Math.random() * loaders.length)]) + document.getElementById('loader-content').style.display = 'initial' + } + window.suggest = suggest + fetch('/api/count').then(async function (result) { + var res = await result.json() + if (res && res.count) { + fetch('/api/stats').then(async function (statsresult) { + var statsres = await statsresult.json() + if (statsres) { + document.getElementById('count').innerHTML = res.count + '
    ' + + '
    STATS
    ' + + '
    • 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) + '
    • ' + + '
    ' + } else { + document.getElementById('count').innerText = res.count + } + }) + fetch('/api/outbox').then(async function (result) { + var res = await result.json() + if (res && res.length > 0) { + var bounce = document.getElementById('bounce'), + host = new URL(window.location.href).host + reg = '(<\/?p>|(https?:\/\/)?' + host + ')' + bounce.innerHTML = res.map(p => `${p.content.replace(new RegExp(reg, 'igm'), '')} ${dayjs().to(p.published)}`).join(' | ') + } + }) + fetch('/api/served').then(async function (result) { + var res = await result.json() + if (res.served) { + var served = document.getElementById('served') + served.innerText = res.served + } + if (res.lastscan) { + var lastscan = document.getElementById('lastscan') + lastscan.innerText = res.lastscan + } + if (res.server) { + var server = document.getElementById('server') + server.innerHTML = '' + res.server + '' + } + if (res.instances) { + var instances = document.getElementById('instances') + instances.innerText = res.instances + } + if (res.peers) { + var peers = document.getElementById('peers') + peers.innerText = res.peers + } + if (res.created) { + var created = document.getElementById('created') + created.innerText = res.created + } + if (res.updated) { + var updated = document.getElementById('updated') + updated.innerText = res.updated + } + }) + } + }) + if (window.location.hash && window.location.hash !== null && window.location.hash !== '#') { + var instance = document.getElementById('instance'), + urlitem = window.location.hash.substring(1) + instance.value = urlitem + if (new URLSearchParams(window.location.search).has('reverse')) { + document.getElementById('reverse').click() + } else { + external = true + listinstance(urlitem, new AbortController()) + } + } else { + ranking() + document.getElementById('instance').focus() + } + var source = new window.EventSource('/api/scan'), + scan = document.getElementById('scan') + source.onmessage = function (event) { + if (event.data) { + if (event.data.length > 0) { + scan.innerText = 'Async Scanning' + event.data + } else { + scan.innerText = 'Async Scanning...' + } + } + } + source.onerror = function (error) { + console.error(error) + } + if (new URLSearchParams(window.location.search).has('matrix')) { + var walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT) + while (walker.nextNode()) { + if (walker.currentNode.textContent.length > 1) { + new Messenger(walker.currentNode) + } + } + var matrix = document.getElementById('matrix') + matrix.href = '/' + matrix.innerText = 'matrix on' + } else { + var matrix = document.getElementById('matrix') + matrix.href = '/?matrix' + matrix.innerText = 'matrix off' + } +}) \ No newline at end of file diff --git a/public/random-text.js b/public/random-text.js new file mode 100644 index 0000000..cdb0a8a --- /dev/null +++ b/public/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(); +} + +window.Messenger = Messenger; \ No newline at end of file diff --git a/served.txt b/served.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/served.txt @@ -0,0 +1 @@ +0 diff --git a/server.js b/server.js new file mode 100644 index 0000000..a60a85e --- /dev/null +++ b/server.js @@ -0,0 +1,146 @@ +const apexinstance = require('./lib/apex'), + apexcustom = require('./lib/apexcustom'), + apiswagger = require('./lib/apiswagger'), + api = require('./lib/api'), + logger = require('./lib/logger'), + swagger = require('./lib/swagger'), + fediblock = require('./lib/fediblock'), + constant = require('./lib/constant'), + http = require('http'), + express = require('express'), + app = express(), + events = require('events'), + { generateKeyPairSync } = require('crypto'), + { MongoClient } = require('mongodb'), + mongoclient = new MongoClient(constant.dburl), + { Client } = require('@elastic/elasticsearch'), + client = new Client({ + node: constant.elasticnode, + pingTimeout: 10000, + requestTimeout: 60000, + retryOnTimeout: true, + maxRetries: 3 + }), + ActivitypubExpress = require('activitypub-express'), + routes = { + actor: '/u/:actor', + object: '/o/:id', + activity: '/s/:id', + inbox: '/u/:actor/inbox', + outbox: '/u/:actor/outbox', + followers: '/u/:actor/followers', + following: '/u/:actor/following', + liked: '/u/:actor/liked', + collections: '/u/:actor/c/:id', + blocked: '/u/:actor/blocked', + rejections: '/u/:actor/rejections', + rejected: '/u/:actor/rejected', + shares: '/s/:id/shares', + likes: '/s/:id/likes' + }, + apex = ActivitypubExpress({ + name: 'Fediblock Instance', + version: '1.0.0', + domain: constant.apexdomain, + actorParam: 'actor', + objectParam: 'id', + activityParam: 'id', + routes, + endpoints: { + proxyUrl: 'https://' + constant.apexdomain + '/proxy' + }, + itemsPerPage: 50 + }), + server = http.createServer(app).listen(4000, () => { + mongoclient.connect() + .then(async () => { + const exists = await client.indices.exists({ index: constant.index }) + if (!exists) { + await client.indices.create({ + index: constant.index, + body: require('./fediblock-mapping.json') + }) + } + apex.store.db = mongoclient.db(constant.dbname) + return apex.store.db + }) + .then(async () => { + try { + const admin = await apex.store.getObject(apex.utils.usernameToIRI('admin'), true) + if (!admin) { + const adminaccount = await apex.createActor('admin', 'Fediblock Admin', 'Fediblock Admin - https://' + constant.apexdomain, + { type: 'Image', mediaType: 'image/png', url: constant.icon }, 'Service'), + keys = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) + adminaccount.publicKey[0].publicKeyPem[0] = keys.publicKey + adminaccount._meta.privateKey = keys.privateKey + await apex.store.saveObject(adminaccount) + const bot = await apex.createActor(constant.nick, 'Fediblock Bot', 'Fediblock #Bot - https://' + constant.apexdomain, + { type: 'Image', mediaType: 'image/png', url: constant.icon }, 'Person'), + keysbot = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) + bot.publicKey[0].publicKeyPem[0] = keysbot.publicKey + bot._meta.privateKey = keysbot.privateKey + await apex.store.saveObject(bot) + apex.systemUser = adminaccount + apex.store.setup(adminaccount) + } else { + apex.systemUser = admin + } + await apex.startDelivery() + console.log('Server listening on ' + server.address().address + ':' + server.address().port) + await fediblock(client, apex, app) + } catch (e) { + console.error(e) + } + }) + }) + +app.locals.scan = new events.EventEmitter() +app.locals.scannum = 0 +app.locals.scantotal = 0 +app.locals.server = '' +app.locals.peers = 0 +app.locals.instances = 0 +app.locals.created = 0 +app.locals.updated = 0 +app.disable('x-powered-by') +app.set('json spaces', 2) +app.set('trust proxy', true) +logger(app) +app.use( + express.json({ type: apex.consts.jsonldTypes }), + express.urlencoded({ extended: true }), + apex +) +apexinstance(app, apex, routes) +apexcustom(app, apex, client) +api(app, apex, client) +apiswagger(app, client) +swagger(app) +app.use(express.static('dist')) + .use((err, req, res, next) => { + if (res.headersSent) { + console.error(err.stack) + return next(err) + } + res.status(500).end('Error') + })