Porównaj commity

...

29 Commity
v2.9.6 ... main

Autor SHA1 Wiadomość Data
Tao Bojlén 6efce18c3b update plausible 2023-06-10 20:45:17 +01:00
Tao Bojlén ed0c28f24e fix nodeinfo for some pixelfed instances 2023-06-10 20:41:38 +01:00
Tao Bojlén de97f6d843 fix frontend builds 2023-06-10 20:24:53 +01:00
Tao Bojlén 9c0bf93420 crawl less often, and fewer statuses 2023-06-10 20:23:07 +01:00
Tao Bojlén a4eaf75c70 add mastodon tests, handle auth'd timelines 2023-06-10 20:18:09 +01:00
Tao Bojlén 8f4193e43f add misskey tests 2023-06-10 19:52:00 +01:00
Tao Bojlén 12b035780e fix nodeinfo and add basic tests 2023-06-10 18:53:10 +01:00
Tao Bojlén 0970e39dea add plausible to frontend 2023-06-10 16:53:09 +01:00
Tao Bojlén b054f78197 make crawl workers configurable 2023-06-10 16:25:22 +01:00
Tao Bojlén ffa0e50966 add mastodon link with rel=me 2023-06-10 15:38:47 +01:00
Tao Bojlén f7a5dbc9d5 add healthcheck 2023-06-10 15:32:53 +01:00
Tao Bojlén f6c754a4ac add more spam instances 2023-06-10 15:19:18 +01:00
Tao Bojlén 96a35c5d9a add activitypub-troll.cf to list of spammers 2023-06-10 15:02:10 +01:00
Tao Bojlén ca831b3831 fix broken instance cache on misses 2023-06-10 14:56:40 +01:00
Tao Bojlén 01ff551516 update appsignal 2023-06-10 14:20:10 +01:00
Tao Bojlén 4cdd03dd6c prep for react-router v6 2023-06-10 14:12:26 +01:00
Tao Bojlén 1ccb2a84a1 Revert "remove elasticsearch"
This reverts commit b7cb7fa685.
2023-06-10 11:40:16 +01:00
Tao Bojlén ba024c9357 update frontend deps 2023-06-10 11:40:09 +01:00
Tao Bojlén 9cae5e58b2 update frontend 2023-06-10 10:37:17 +01:00
Tao Bojlén 4d4193ff49 fix crawls of well-connected instances 2023-06-10 09:36:03 +01:00
Tao Bojlén b7cb7fa685 remove elasticsearch 2023-06-08 23:27:13 +01:00
Tao Bojlén 332d12e1a4
Merge pull request #1 from fediverse-space/deploy-images
release docker image to ghcr
2023-06-08 23:18:23 +01:00
Tao Bojlén 98b7448291 release docker images to ghcr 2023-06-08 23:18:06 +01:00
Tao Bojlén 43745881dd release docker images to ghcr 2023-06-08 23:09:52 +01:00
Tao Bojlén 0234a465f2 update to phoenix 1.7 2023-06-04 19:02:21 +01:00
Tao Bojlén e7a6c0a988 Squashed commit of the following:
commit 09d8229ab3ed8a1d12dfc2d4f96d6cc6f9d317af
Author: Tao Bojlén <66130243+taobojlen@users.noreply.github.com>
Date:   Sun Jun 4 17:28:30 2023 +0100

    remove autodeploy
2023-06-04 17:30:45 +01:00
Tao Bojlén 9115d29a88 update backend 2023-06-04 16:41:15 +01:00
Inex Code aacc8574d8 Fix friendica recognition
Closes #107
2021-09-18 19:21:16 +03:00
Inex Code 7bb8bd0a8a Cherry-picking fork fixes
* Update dependencies
* Fix mastodon crawling
* Add basic Smithereen support
* Remove twilio
* Remove react-axe (created a ton of lags)
2021-09-18 18:31:32 +03:00
83 zmienionych plików z 9275 dodań i 24641 usunięć

42
.github/workflows/deploy.yml vendored 100644
Wyświetl plik

@ -0,0 +1,42 @@
name: Build image
on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
# Permissions to use OIDC token authentication
permissions:
contents: read
id-token: write
# Allows pushing to the GitHub Container Registry
packages: write
steps:
- uses: actions/checkout@v3
- uses: depot/setup-action@v1
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build & push
uses: depot/build-push-action@v1
with:
project: rktsv8c4sk
context: backend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

43
.github/workflows/elixir.yml vendored 100644
Wyświetl plik

@ -0,0 +1,43 @@
name: Elixir CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/setup-elixir@885971a72ed1f9240973bd92ab57af8c1aa68f24
with:
elixir-version: '1.12.2' # Define the elixir version [required]
otp-version: '24.0.4' # Define the OTP version [required]
- name: Restore dependencies cache
uses: actions/cache@v2
with:
working-directory: ./backend
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
working-directory: ./backend
run: |
mix local.hex --force
mix local.rebar --force
mix deps.get
- name: Compile dependencies
working-directory: ./backend
run: mix deps.compile
- name: Run Credo
working-directory: ./backend
run: mix credo --strict
- name: Run sobelow
working-directory: ./backend
run: mix sobelow --config

37
.github/workflows/main.yml vendored 100644
Wyświetl plik

@ -0,0 +1,37 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [master]
pull_request:
branches: [master]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.3.0
- name: Setup deps
working-directory: ./frontend
run: npm install
- name: Lint
working-directory: ./frontend
run: npm run lint

4
.gitignore vendored
Wyświetl plik

@ -1,10 +1,12 @@
*.csv
.idea/
*.gexf
data/
*.class
backend/.sobelow
node_modules/
/frontend/build/
/frontend/dist/
# Environments
.env

Wyświetl plik

@ -5,19 +5,19 @@ test-frontend:
- cd frontend
stage: test
script:
- yarn install
- yarn lint
- npm install
- npm run lint
cache:
paths:
- frontend/node_modules/
- frontend/.yarn
- .npm/
only:
changes:
- frontend/**/*
test-backend:
stage: test
image: elixir:1.10
image: elixir:1.14
variables:
MIX_ENV: test
only:
@ -36,28 +36,3 @@ test-backend:
paths:
- backend/deps/
- backend/_build/
deploy-backend-production:
stage: deploy
environment:
name: production
url: https://phoenix.api.fediverse.space
image: ilyasemenov/gitlab-ci-git-push
only:
- master
except:
- schedules
script:
- git-push dokku@api.fediverse.space:phoenix master
deploy-gephi-production:
stage: deploy
image: ilyasemenov/gitlab-ci-git-push
environment:
name: production
only:
- master
except:
- schedules
script:
- git-push dokku@api.fediverse.space:gephi master

Wyświetl plik

@ -4,28 +4,30 @@ This is an overview of the external software components (libraries, etc.) that
are used in fediverse.space.
## Backend
### Crawler and API
* [Elixir](https://elixir-lang.org/) (the language)
* [Phoenix](https://phoenixframework.org/) (the web framework)
* See [/backend/mix.env](/backend/mix.env) for a complete overview of
- [Elixir](https://elixir-lang.org/) (the language)
- [Phoenix](https://phoenixframework.org/) (the web framework)
- See [/backend/mix.env](/backend/mix.env) for a complete overview of
dependencies
### Graph layout
* Java (the language)
* Gradle (to build)
* [Gephi toolkit](https://gephi.org/toolkit/)
- Java (the language)
- Gradle (to build)
- [Gephi toolkit](https://gephi.org/toolkit/)
## Frontend
* [React](https://reactjs.org/) (the UI framework)
* [Blueprint](https://blueprintjs.com/) (a collection of pre-existing UI components)
* [Cytoscape.js](http://js.cytoscape.org/) (for graph visualization)
* See [/frontend/package.json](/frontend/package.json) for a complete overview
- [React](https://reactjs.org/) (the UI framework)
- [Blueprint](https://blueprintjs.com/) (a collection of pre-existing UI components)
- [Cytoscape.js](http://js.cytoscape.org/) (for graph visualization)
- See [/frontend/package.json](/frontend/package.json) for a complete overview
of dependencies
## Other
* [Docker](https://www.docker.com/) and
[docker-compose](https://docs.docker.com/compose/overview/)
* The backend is deployed using [Dokku](http://dokku.viewdocs.io/dokku/).
* The frontend is hosted on [Netlify](https://www.netlify.com/)
* [GitLab](https://gitlab.com/) and GitLab CI/CD are used for project management and CI/CD.
- [Docker](https://www.docker.com/) and
[docker-compose](https://docs.docker.com/compose/overview/)
- [GitLab](https://gitlab.com/) and GitLab CI/CD are used for project management and CI/CD.

Wyświetl plik

@ -20,37 +20,31 @@ Read the latest updates on Mastodon: [@fediversespace](https://mastodon.social/@
## Requirements
Note: examples here use `podman`. In most cases you should be able to replace `podman` with `docker`.
Though containerized, backend development is easiest if you have the following installed.
You'll need the following to work on fediverse.space:
- For the crawler + API:
- Elixir
- Postgres
- Elasticsearch
- For laying out the graph:
- Java
- For the frontend:
- Node.js
- Yarn
## Running it
### Backend
- `cp example.env .env` and modify environment variables as required
- `podman build gephi && podman build phoenix`
- `podman run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:6.8.9`
- If you've `run` this container previously, use `podman start elasticsearch`
- `podman run --name postgres -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=postgres" -p 5432:5432 postgres:12`
- `podman-compose -f compose.backend-services.yml -f compose.phoenix.yml`
- `docker-compose up`
- Create the elasticsearch index:
- `iex -S mix app.start`
- `Elasticsearch.Index.hot_swap(Backend.Elasticsearch.Cluster, :instances)`
### Frontend
- `cd frontend && yarn install`
- `yarn start`
- `cd frontend && npm install`
- `npm start`
## Commands
@ -60,7 +54,7 @@ Though containerized, backend development is easiest if you have the following i
### Frontend
- `yarn build` creates an optimized build for deployment
- `npm run build` creates an optimized build for deployment
## Privacy
@ -88,14 +82,19 @@ You don't have to follow these instructions, but it's one way to set up a contin
- `dokku elasticsearch:create fediverse`
- `dokku elasticsearch:link fediverse phoenix`
6. Update the backend configuration. In particular, change the `user_agent` in [config.exs](/backend/config/config.exs) to something descriptive.
7. Push the apps, e.g. `git push dokku@<DOMAIN>:phoenix` (note that the first push cannot be from the CD pipeline).
8. Set up SSL for the Phoenix app
6. Set the build dirs
- `dokku letsencrypt phoenix`
- `dokku builder:set phoenix build-dir backend`
- `dokku builder:set gephi build-dir gephi`
7. Update the backend configuration. In particular, change the `user_agent` in [config.exs](/backend/config/config.exs) to something descriptive.
8. Push the apps, e.g. `git push dokku@<DOMAIN>:phoenix` (note that the first push cannot be from the CD pipeline).
9. Set up SSL for the Phoenix app
- `dokku letsencrypt:enable phoenix`
- `dokku letsencrypt:cron-job --add`
9. Set up a cron job for the graph layout (use the `dokku` user). E.g.
10. Set up a cron job for the graph layout (use the `dokku` user). E.g.
```
SHELL=/bin/bash

209
backend/.credo.exs 100644
Wyświetl plik

@ -0,0 +1,209 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: [
"**/*.{ex,exs}"
],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: true,
#
# To modify the timeout for parsing files, change this value:
#
parse_timeout: 5000,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: %{
enabled: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
{Credo.Check.Design.TagFIXME, []},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.Dbg, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.WrongTestFileExtension, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.UnsafeExec, []}
],
disabled: [
#
# Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)
#
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
# and be sure to use `mix credo --strict` to see low priority checks)
#
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
{Credo.Check.Consistency.UnusedVariableNames, []},
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []},
{Credo.Check.Readability.OneArityFunctionInPipe, []},
{Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Readability.OnePipePerLine, []},
{Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PassAsyncInTestCases, []},
{Credo.Check.Refactor.PipeChainStart, []},
{Credo.Check.Refactor.RejectFilter, []},
{Credo.Check.Refactor.VariableRebinding, []},
{Credo.Check.Warning.LazyLogging, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []},
#
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
}
]
}

Wyświetl plik

@ -1,5 +1,6 @@
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
plugins: [Phoenix.LiveView.HTMLFormatter],
subdirectories: ["priv/*/migrations"]
]

1
backend/CHECKS 100644
Wyświetl plik

@ -0,0 +1 @@
/health OK

Wyświetl plik

@ -1,7 +1,7 @@
FROM elixir:1.10-alpine as build
FROM elixir:1.14-alpine as build
# install build dependencies
RUN apk add --update git build-base
RUN apk add --update git build-base
# prepare build dir
RUN mkdir /app
@ -36,8 +36,8 @@ COPY rel rel
RUN mix release
# prepare release image
FROM alpine:3.9 AS app
RUN apk add --update bash openssl
FROM alpine:3.17 AS app
RUN apk add --update bash openssl libstdc++ build-base
RUN mkdir /app
WORKDIR /app
@ -46,6 +46,7 @@ ENV APP_NAME=backend
COPY --from=build /app/_build/prod/rel/${APP_NAME} ./
COPY Procfile ./
COPY CHECKS ./
RUN chown -R nobody: /app
USER nobody

Wyświetl plik

@ -2,7 +2,7 @@
## Notes
- This project requires Elixir >= 1.10.
- This project requires Elixir >= 1.14.
- Run with `SKIP_CRAWL=true` to just run the server (useful for working on the API without also crawling)
- This project is automatically scanned for potential vulnerabilities with [Sobelow](https://sobelow.io/).

Wyświetl plik

@ -13,9 +13,11 @@ config :backend,
# Configures the endpoint
config :backend, BackendWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "XL4NKGBN9lZMrQbMEI1KJOlwAt8S7younVJl90TdAgzmwyapr3g7BRYSNYvX0sZ9",
secret_key_base: System.get_env("SECRET_KEY_BASE"),
render_errors: [view: BackendWeb.ErrorView, accepts: ~w(json)]
config :backend, :http, Backend.Http
config :backend, Backend.Repo, queue_target: 5000
config :backend, Backend.Elasticsearch.Cluster,
@ -41,13 +43,15 @@ config :backend, Graph.Cache,
# 1 hour
gc_interval: 3600
config :ex_twilio,
account_sid: System.get_env("TWILIO_ACCOUNT_SID"),
auth_token: System.get_env("TWILIO_AUTH_TOKEN")
config :backend, Backend.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: System.get_env("SENDGRID_API_KEY")
adapter: Swoosh.Adapters.SMTP,
relay: System.get_env("MAILER_RELAY"),
username: System.get_env("MAILER_USERNAME"),
password: System.get_env("MAILER_PASSWORD"),
ssl: true,
tls: :always,
auth: :always,
port: 465
config :backend, Mastodon.Messenger,
domain: System.get_env("MASTODON_DOMAIN"),
@ -55,15 +59,23 @@ config :backend, Mastodon.Messenger,
config :backend, :crawler,
status_age_limit_days: 28,
status_count_limit: 5000,
status_count_limit: 1000,
personal_instance_threshold: 10,
crawl_interval_mins: 30,
crawl_interval_mins: 60,
crawl_workers: 100,
blacklist: [
# spam
"gab.best",
# spam
"4chan.icu",
# spam
"activitypub-troll.cf",
# spam
"misskey-forkbomb.cf",
# spam
"repl.co",
# malicious?
"ignorelist.com",
# *really* doesn't want to be listed on fediverse.space
"pleroma.site",
# dummy instances used for pleroma CI
@ -72,7 +84,6 @@ config :backend, :crawler,
user_agent: "fediverse.space crawler",
require_bidirectional_mentions: false,
admin_phone: System.get_env("ADMIN_PHONE"),
twilio_phone: System.get_env("TWILIO_PHONE"),
admin_email: System.get_env("ADMIN_EMAIL")
config :backend, Backend.Scheduler,
@ -89,9 +100,7 @@ config :backend, Backend.Scheduler,
{"0 */3 * * *", {Backend.Scheduler, :check_for_spam_instances, []}}
]
config :phoenix, :template_engines,
eex: Appsignal.Phoenix.Template.EExEngine,
exs: Appsignal.Phoenix.Template.ExsEngine
config :backend, :environment, Mix.env()
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.

Wyświetl plik

@ -17,7 +17,11 @@ config :backend, Backend.Repo,
config :backend, Backend.Elasticsearch.Cluster,
url: System.get_env("ELASTICSEARCH_URL") || "http://localhost:9200"
config :appsignal, :config, revision: System.get_env("GIT_REV")
config :appsignal, :config,
otp_app: :backend,
name: "fediverse.space",
active: true,
revision: System.get_env("GIT_REV")
port = String.to_integer(System.get_env("PORT") || "4000")
@ -28,19 +32,20 @@ config :backend, BackendWeb.Endpoint,
secret_key_base: System.get_env("SECRET_KEY_BASE"),
server: true
config :ex_twilio,
account_sid: System.get_env("TWILIO_ACCOUNT_SID"),
auth_token: System.get_env("TWILIO_AUTH_TOKEN")
config :backend, :crawler,
admin_phone: System.get_env("ADMIN_PHONE"),
twilio_phone: System.get_env("TWILIO_PHONE"),
admin_email: System.get_env("ADMIN_EMAIL"),
frontend_domain: System.get_env("FRONTEND_DOMAIN")
frontend_domain: System.get_env("FRONTEND_DOMAIN"),
crawl_workers: String.to_integer(System.get_env("CRAWL_WORKERS") || "100")
config :backend, Backend.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: System.get_env("SENDGRID_API_KEY")
adapter: Swoosh.Adapters.SMTP,
relay: System.get_env("MAILER_RELAY"),
username: System.get_env("MAILER_USERNAME"),
password: System.get_env("MAILER_PASSWORD"),
ssl: true,
auth: :always,
port: 465
config :backend, Mastodon.Messenger,
domain: System.get_env("MASTODON_DOMAIN"),

Wyświetl plik

@ -16,3 +16,7 @@ config :backend, Backend.Repo,
database: "backend_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
config :appsignal, :config, active: false
config :backend, :crawler, status_count_limit: 5

Wyświetl plik

@ -7,14 +7,6 @@ defmodule Backend.Application do
import Backend.Util
def start(_type, _args) do
:telemetry.attach(
"appsignal-ecto",
[:backend, :repo, :query],
&Appsignal.Ecto.handle_event/4,
nil
)
crawl_worker_count = get_config(:crawl_workers)
children = [
@ -39,9 +31,11 @@ defmodule Backend.Application do
]
children =
case Enum.member?(["true", 1, "1"], System.get_env("SKIP_CRAWL")) do
true -> children
false -> children ++ [Backend.Crawler.StaleInstanceManager]
if Enum.member?(["true", 1, "1"], System.get_env("SKIP_CRAWL")) or
Application.get_env(:backend, :environment) == :test do
children
else
children ++ [Backend.Crawler.StaleInstanceManager]
end
add_appsignal_probes()

Wyświetl plik

@ -18,7 +18,8 @@ defmodule Backend.Crawler.ApiCrawler do
# {domain, type} e.g. {"gab.com", "reject"}
@type federation_restriction :: {String.t(), String.t()}
@type instance_type :: :mastodon | :pleroma | :gab | :misskey | :gnusocial
@type instance_type ::
:mastodon | :pleroma | :gab | :misskey | :gnusocial | :smithereen | :friendica
defstruct [
:version,

Wyświetl plik

@ -117,8 +117,8 @@ defmodule Backend.Crawler do
try do
%Crawler{state | result: curr.crawl(domain, result), found_api?: true}
rescue
e in HTTPoison.Error ->
Map.put(state, :error, "HTTPoison error: " <> HTTPoison.Error.message(e))
e in Backend.HttpBehaviour.Error ->
Map.put(state, :error, "HTTP error: " <> e.message)
e in Jason.DecodeError ->
Map.put(state, :error, "Jason DecodeError: " <> Jason.DecodeError.message(e))
@ -237,9 +237,12 @@ defmodule Backend.Crawler do
Enum.map(result.federation_restrictions, fn {domain, _restriction_type} -> domain end)
)
|> Enum.map(&%{domain: &1, inserted_at: now, updated_at: now, next_crawl: now})
|> Enum.chunk_every(5000)
Instance
|> Repo.insert_all(new_instances, on_conflict: :nothing, conflict_target: :domain)
new_instances
|> Enum.each(fn chunk ->
Repo.insert_all(Instance, chunk, on_conflict: :nothing, conflict_target: :domain)
end)
Repo.transaction(fn ->
## Save peer relationships ##
@ -276,9 +279,10 @@ defmodule Backend.Crawler do
updated_at: now
}
)
|> Enum.chunk_every(5000)
InstancePeer
|> Repo.insert_all(new_instance_peers)
new_instance_peers
|> Enum.each(fn chunk -> Repo.insert_all(InstancePeer, chunk) end)
end)
## Save federation restrictions ##

Wyświetl plik

@ -51,7 +51,7 @@ defmodule Backend.Crawler.Crawlers.Friendica do
|> Map.merge(nodeinfo_result)
peers =
case get_and_decode("https://#{domain}/poco/@server") do
case http_client().get_and_decode("https://#{domain}/poco/@server") do
{:ok, p} -> p
{:error, _err} -> []
end
@ -71,7 +71,7 @@ defmodule Backend.Crawler.Crawlers.Friendica do
end
defp get_statistics(domain) do
get_and_decode("https://#{domain}/statistics.json")
http_client().get_and_decode("https://#{domain}/statistics.json")
end
defp to_domain(url) do

Wyświetl plik

@ -14,7 +14,7 @@ defmodule Backend.Crawler.Crawlers.GnuSocial do
if nodeinfo_result != nil do
Map.get(nodeinfo_result, :instance_type) == :gnusocial
else
case get_and_decode("https://#{domain}/api/statuses/public_timeline.json") do
case http_client().get_and_decode("https://#{domain}/api/statuses/public_timeline.json") do
{:ok, statuses} -> is_list(statuses)
{:error, _other} -> false
end
@ -86,7 +86,7 @@ defmodule Backend.Crawler.Crawlers.GnuSocial do
Logger.debug("Crawling #{endpoint}")
statuses = get_and_decode!(endpoint)
statuses = http_client().get_and_decode!(endpoint)
# Filter to statuses that are in the correct timeframe
filtered_statuses =

Wyświetl plik

@ -12,10 +12,19 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
@impl ApiCrawler
def is_instance_type?(domain, result) do
# We might already know that this is a Pleroma instance from nodeinfo
if result != nil and Map.get(result, :instance_type) == :pleroma do
true
if result != nil do
cond do
# for pleroma and smithereen, the instance_type will get overwritten
# with the correct value -- but we still want to return true here
# since they are compatible with the mastodon API
Map.get(result, :instance_type) == :pleroma -> true
Map.get(result, :instance_type) == :smithereen -> true
Map.get(result, :instance_type) == :mastodon -> true
Map.get(result, :instance_type) == :friendica -> false
true -> false
end
else
case get_and_decode("https://#{domain}/api/v1/instance") do
case http_client().get_and_decode("https://#{domain}/api/v1/instance") do
{:ok, %{"title" => _title}} -> true
_other -> false
end
@ -35,7 +44,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
@impl ApiCrawler
def crawl(domain, nodeinfo) do
instance = get_and_decode!("https://#{domain}/api/v1/instance")
instance = http_client().get_and_decode!("https://#{domain}/api/v1/instance")
user_count = get_in(instance, ["stats", "user_count"])
if is_above_user_threshold?(user_count) or has_opted_in?(domain) do
@ -59,9 +68,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
{interactions, statuses_seen} = get_interactions(domain)
Logger.debug(
"#{domain}: found #{
interactions |> Map.values() |> Enum.reduce(0, fn count, acc -> count + acc end)
} mentions in #{statuses_seen} statuses."
"#{domain}: found #{interactions |> Map.values() |> Enum.reduce(0, fn count, acc -> count + acc end)} mentions in #{statuses_seen} statuses."
)
Map.merge(
@ -93,16 +100,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
interactions \\ %{},
statuses_seen \\ 0
) do
# If `statuses_seen == 0`, it's the first call of this function, which means we want to query the database for the
# most recent status we have.
min_timestamp =
if statuses_seen == 0 do
get_last_crawl_timestamp(domain)
else
min_timestamp
end
endpoint = "https://#{domain}/api/v1/timelines/public?local=true"
endpoint = "https://#{domain}/api/v1/timelines/public?local=true&limit=40"
endpoint =
if max_id do
@ -113,7 +111,26 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
Logger.debug("Crawling #{endpoint}")
statuses = get_and_decode!(endpoint)
case http_client().get_and_decode(endpoint) do
{:ok, statuses} ->
handle_statuses(statuses, domain, min_timestamp, interactions, statuses_seen)
# if there's an error (e.g. because the timeline prevents unauthenticated access)
# then stop here
{:error, _} ->
{interactions, statuses_seen}
end
end
defp handle_statuses(statuses, domain, min_timestamp, interactions, statuses_seen) do
# If `statuses_seen == 0`, it's the first call of this function, which means we want to query the database for the
# most recent status we have.
min_timestamp =
if statuses_seen == 0 do
get_last_crawl_timestamp(domain)
else
min_timestamp
end
filtered_statuses =
statuses
@ -157,7 +174,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
defp get_peers(domain) do
# servers may not publish peers
case get_and_decode("https://#{domain}/api/v1/instance/peers") do
case http_client().get_and_decode("https://#{domain}/api/v1/instance/peers") do
{:ok, peers} -> peers
{:error, _err} -> []
end
@ -178,8 +195,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
fields =
account["fields"]
|> Enum.map(fn %{"name" => name, "value" => value} -> name <> value end)
|> Enum.join("")
|> Enum.map_join("", fn %{"name" => name, "value" => value} -> name <> value end)
# this also means that any users who mentioned ethnobotany in their profiles will be excluded lol ¯\_(ツ)_/¯
(account["note"] <> fields)
@ -230,6 +246,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
defp get_instance_type(instance_stats) do
cond do
Map.get(instance_stats, "version") |> String.downcase() =~ "pleroma" -> :pleroma
Map.get(instance_stats, "version") |> String.downcase() =~ "smithereen" -> :smithereen
is_gab?(instance_stats) -> :gab
true -> :mastodon
end

Wyświetl plik

@ -3,6 +3,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
Crawler for Misskey servers.
"""
alias Backend.Crawler.ApiCrawler
alias Backend.Http
@behaviour ApiCrawler
import Backend.Crawler.Util
@ -37,7 +38,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
@impl ApiCrawler
def crawl(domain, nodeinfo) do
with {:ok, %{"originalUsersCount" => user_count, "originalNotesCount" => status_count}} <-
post_and_decode("https://#{domain}/api/stats") do
http_client().post_and_decode("https://#{domain}/api/stats") do
if is_above_user_threshold?(user_count) or has_opted_in?(domain) do
Map.merge(nodeinfo, crawl_large_instance(domain, user_count, status_count))
else
@ -97,7 +98,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
endpoint = "https://#{domain}/api/notes/local-timeline"
params = %{
limit: 20
limit: 100
}
params =
@ -109,7 +110,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
Logger.debug("Crawling #{endpoint} with untilId=#{until_id}")
statuses = post_and_decode!(endpoint, Jason.encode!(params))
statuses = http_client().post_and_decode!(endpoint, params)
filtered_statuses =
statuses
@ -153,9 +154,9 @@ defmodule Backend.Crawler.Crawlers.Misskey do
end
@spec get_version_and_description(String.t()) ::
{:ok, {String.t(), String.t()}} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
{:ok, {String.t(), String.t()}} | {:error, Jason.DecodeError.t() | Http.Error.t()}
defp get_version_and_description(domain) do
case post_and_decode("https://#{domain}/api/meta") do
case http_client().post_and_decode("https://#{domain}/api/meta") do
{:ok, %{"version" => version, "description" => description}} ->
{:ok, {version, description}}
@ -166,7 +167,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
@spec get_peers(String.t()) :: {:ok, [String.t()]} | {:error, Jason.DecodeError.t()}
defp get_peers(domain) do
case get_and_decode("https://#{domain}/api/v1/instance/peers") do
case http_client().get_and_decode("https://#{domain}/api/v1/instance/peers") do
{:ok, peers} -> {:ok, peers}
{:error, _} -> {:ok, []}
end

Wyświetl plik

@ -5,6 +5,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
"""
alias Backend.Crawler.ApiCrawler
alias Backend.Http
require Logger
import Backend.Util
import Backend.Crawler.Util
@ -13,7 +14,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
@impl ApiCrawler
def allows_crawling?(domain) do
[
".well-known/nodeinfo"
"/.well-known/nodeinfo"
]
|> Enum.map(fn endpoint -> "https://#{domain}#{endpoint}" end)
|> urls_are_crawlable?()
@ -36,26 +37,40 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
end
@spec get_nodeinfo_url(String.t()) ::
{:ok, String.t()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
{:ok, String.t()} | {:error, Jason.DecodeError.t() | Http.Error.t() | :invalid_body}
defp get_nodeinfo_url(domain) do
case get_and_decode("https://#{domain}/.well-known/nodeinfo") do
{:ok, response} -> {:ok, process_nodeinfo_url(response)}
{:error, err} -> {:error, err}
with {:ok, response} <-
http_client().get_and_decode("https://#{domain}/.well-known/nodeinfo"),
{:ok, nodeinfo_url} <- process_nodeinfo_url(response) do
{:ok, nodeinfo_url}
else
{:error, error} -> {:error, error}
:error -> {:error, :invalid_body}
end
end
@spec process_nodeinfo_url(any()) :: String.t()
@spec process_nodeinfo_url(any()) :: {:ok, String.t()} | :error
defp process_nodeinfo_url(response) do
response
|> Map.get("links")
|> Enum.filter(fn %{"rel" => rel} -> is_compatible_nodeinfo_version?(rel) end)
|> Kernel.hd()
|> Map.get("href")
links =
response
|> Map.get("links", [])
|> Enum.filter(fn %{"rel" => rel} -> is_compatible_nodeinfo_version?(rel) end)
if Enum.empty?(links) do
:error
else
href =
links
|> Kernel.hd()
|> Map.get("href")
{:ok, href}
end
end
@spec get_nodeinfo(String.t()) :: ApiCrawler.t()
defp get_nodeinfo(nodeinfo_url) do
case get_and_decode(nodeinfo_url) do
case http_client().get_and_decode(nodeinfo_url) do
{:ok, nodeinfo} -> {:ok, process_nodeinfo(nodeinfo)}
{:error, err} -> {:error, err}
end
@ -70,7 +85,9 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
description =
[
get_in(nodeinfo, ["metadata", "description"]),
get_in(nodeinfo, ["metadata", "nodeDescription"])
get_in(nodeinfo, ["metadata", "nodeDescription"]),
# pixelfed
get_in(nodeinfo, ["metadata", "config", "site", "description"])
]
|> Enum.filter(fn d -> d != nil end)
|> Enum.at(0)
@ -81,8 +98,8 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
ApiCrawler.get_default(),
%{
description: description,
user_count: user_count,
status_count: get_in(nodeinfo, ["usage", "localPosts"]),
user_count: handle_count(user_count),
status_count: nodeinfo |> get_in(["usage", "localPosts"]) |> handle_count(),
instance_type: type,
version: get_in(nodeinfo, ["software", "version"]),
federation_restrictions: get_federation_restrictions(nodeinfo)
@ -129,6 +146,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
"accept"
])
|> Enum.flat_map(fn {type, domains} ->
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
Enum.map(domains, fn domain -> {domain, type} end)
end)
|> Enum.concat(quarantined_domains)
@ -136,4 +154,14 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
quarantined_domains
end
end
# handle a count that may be formatted as a string or an integer
defp handle_count(count) do
if is_integer(count) do
count
else
{count, _rem} = Integer.parse(count)
count
end
end
end

Wyświetl plik

@ -17,6 +17,7 @@ defmodule Backend.Crawler.StaleInstanceManager do
@impl true
def init(_opts) do
Logger.info("Starting crawler manager...")
Backend.Repo.start_link()
instance_count =
Instance

Wyświetl plik

@ -0,0 +1,93 @@
defmodule Backend.Http do
@moduledoc """
A wrapper around HTTPoison. Using this wrapper makes it easy for us
to mock web responses in tests, and we can easily switch out HTTPoison for
another library if we want to.
"""
@behaviour Backend.HttpBehaviour
alias Backend.HttpBehaviour.Error
import Backend.Util
@doc """
GETs from the given URL and returns the JSON-decoded response.
If the response is unsuccessful and a default value is given, this returns the default value.
Otherwise, unsuccessful responses return an error.
"""
@impl true
def get_and_decode(url, pool \\ :default, timeout \\ 15_000, default \\ nil) do
case HTTPoison.get(url, [{"User-Agent", get_config(:user_agent)}],
hackney: [pool: pool],
recv_timeout: timeout,
timeout: timeout
) do
{:ok, %HTTPoison.Response{body: body, status_code: status_code}}
when status_code >= 200 and status_code <= 299 ->
decode_body(body)
{:ok, %HTTPoison.Response{body: body, status_code: status_code}} ->
if not is_nil(default) do
{:ok, default}
else
{:error,
%Error{
message: "HTTP request failed with status code #{status_code}",
status_code: status_code,
body: body
}}
end
{:error, %HTTPoison.Error{} = error} ->
{:error, %Error{message: HTTPoison.Error.message(error)}}
end
end
@impl true
def get_and_decode!(url, pool \\ :default, timeout \\ 15_000, default \\ nil) do
case get_and_decode(url, pool, timeout, default) do
{:ok, decoded} -> decoded
{:error, error} -> raise error
end
end
@doc """
POSTs to the given URL with the given body and returns the JSON-decoded response.
The given body is JSON-encoded before sending.
"""
@impl true
def post_and_decode(url, body \\ %{}) do
case HTTPoison.post(url, Jason.encode!(body), [
{"User-Agent", get_config(:user_agent)},
{"Content-Type", "application/json"}
]) do
{:ok, %HTTPoison.Response{body: body}} ->
decode_body(body)
{:error, %HTTPoison.Error{} = error} ->
{:error, %Error{message: HTTPoison.Error.message(error)}}
end
end
@impl true
def post_and_decode!(url, body \\ %{}) do
case post_and_decode(url, body) do
{:ok, decoded} ->
decoded
{:error, error} ->
raise error
end
end
defp decode_body(body) do
with {:ok, decoded} <- Jason.decode(body) do
if is_map(decoded) and (Map.has_key?(decoded, "errors") or Map.has_key?(decoded, "error")) do
{:error, %Error{message: "API error: " <> body}}
else
{:ok, decoded}
end
else
{:error, error} -> {:error, error}
end
end
end

Wyświetl plik

@ -0,0 +1,23 @@
defmodule Backend.HttpBehaviour do
@moduledoc """
This module defines the behavior for HTTP requests.
"""
defmodule Error do
defstruct message: nil, status_code: nil, body: nil
@type t :: %__MODULE__{message: String.t(), status_code: integer | nil, body: term | nil}
end
@type response :: {:ok, Response.t()} | {:error, __MODULE__.Error.t() | Jason.DecodeError.t()}
@callback get_and_decode(String.t()) :: response
@callback get_and_decode(String.t(), Atom.t(), Integer.t(), any()) :: response
@callback get_and_decode!(String.t()) :: Response.t()
@callback get_and_decode!(String.t(), Atom.t(), Integer.t(), any()) :: Response.t()
@callback post_and_decode(String.t()) :: response()
@callback post_and_decode(String.t(), String.t()) :: response()
@callback post_and_decode!(String.t()) :: Response.t()
@callback post_and_decode!(String.t(), String.t()) :: Response.t()
end

Wyświetl plik

@ -14,7 +14,7 @@ defmodule Backend.Release do
]
# Ecto repos to start, if any
@repos Application.get_env(:backend, :ecto_repos, [])
@repos Application.compile_env(:backend, :ecto_repos, [])
# Elasticsearch clusters to start
@clusters [Backend.Elasticsearch.Cluster]
# Elasticsearch indexes to build

Wyświetl plik

@ -269,18 +269,14 @@ defmodule Backend.Scheduler do
if length(potential_spam_instances) > 0 do
message =
potential_spam_instances
|> Enum.map(fn %{count: count, base_domain: base_domain} ->
|> Enum.map_join("\n", fn %{count: count, base_domain: base_domain} ->
"* #{count} new at #{base_domain}"
end)
|> Enum.join("\n")
|> (fn lines ->
"fediverse.space detected the following potential spam domains from the last #{
hour_range
} hours:\n#{lines}"
"fediverse.space detected the following potential spam domains from the last #{hour_range} hours:\n#{lines}"
end).()
Logger.info(message)
send_admin_sms(message)
AdminEmail.send("Potential spam", message)
else
Logger.debug("Did not find potential spam instances.")

Wyświetl plik

@ -113,21 +113,6 @@ defmodule Backend.Util do
end)
end
@doc """
Sends an SMS to the admin phone number if configured.
"""
def send_admin_sms(body) do
if get_config(:admin_phone) != nil and get_config(:twilio_phone) != nil do
ExTwilio.Message.create(
to: get_config(:admin_phone),
from: get_config(:twilio_phone),
body: body
)
else
Logger.info("Could not send SMS to admin; not configured.")
end
end
@spec clean_domain(String.t()) :: String.t()
def clean_domain(domain) do
cleaned =
@ -158,58 +143,12 @@ defmodule Backend.Util do
map |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
end
@doc """
Gets and decodes a HTTP response.
"""
@spec get_and_decode(String.t(), Atom.t(), Integer.t()) ::
{:ok, any()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
def get_and_decode(url, pool \\ :crawler, timeout \\ 15_000) do
case HTTPoison.get(url, [{"User-Agent", get_config(:user_agent)}],
hackney: [pool: pool],
recv_timeout: timeout,
timeout: timeout
) do
{:ok, %{status_code: 200, body: body}} -> Jason.decode(body)
{:ok, _} -> {:error, %HTTPoison.Error{reason: "Non-200 response"}}
{:error, err} -> {:error, err}
end
end
@spec get_and_decode!(String.t()) :: any()
def get_and_decode!(url) do
case get_and_decode(url) do
{:ok, decoded} -> decoded
{:error, error} -> raise error
end
end
@doc """
POSTS to a HTTP endpoint and decodes the JSON response.
"""
@spec post_and_decode(String.t(), String.t()) ::
{:ok, any()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
def post_and_decode(url, body \\ "") do
case HTTPoison.post(url, body, [{"User-Agent", get_config(:user_agent)}],
hackney: [pool: :crawler],
recv_timeout: 15_000,
timeout: 15_000
) do
{:ok, %{status_code: 200, body: response_body}} -> Jason.decode(response_body)
{:ok, _} -> {:error, %HTTPoison.Error{reason: "Non-200 response"}}
{:error, err} -> {:error, err}
end
end
@spec post_and_decode!(String.t(), String.t()) :: any()
def post_and_decode!(url, body \\ "") do
case post_and_decode(url, body) do
{:ok, decoded} -> decoded
{:error, error} -> raise error
end
end
@spec is_valid_domain?(String.t()) :: boolean
def is_valid_domain?(domain) do
Regex.match?(~r/^[\pL\d\.\-_]+\.[a-zA-Z]+$/, domain)
end
def http_client() do
Application.get_env(:backend, :http, Backend.Http)
end
end

Wyświetl plik

@ -17,6 +17,8 @@ defmodule BackendWeb do
and import those modules here.
"""
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def controller do
quote do
use Phoenix.Controller, namespace: BackendWeb
@ -24,6 +26,8 @@ defmodule BackendWeb do
import Plug.Conn
import BackendWeb.Gettext
alias BackendWeb.Router.Helpers, as: Routes
unquote(verified_routes())
end
end
@ -39,6 +43,8 @@ defmodule BackendWeb do
import BackendWeb.ErrorHelpers
import BackendWeb.Gettext
alias BackendWeb.Router.Helpers, as: Routes
unquote(verified_routes())
end
end
@ -57,6 +63,15 @@ defmodule BackendWeb do
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: BackendWeb.Endpoint,
router: BackendWeb.Router,
statics: BackendWeb.static_paths()
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""

Wyświetl plik

@ -5,7 +5,7 @@ defmodule BackendWeb.AdminLoginController do
alias Backend.Mailer.UserEmail
alias Mastodon.Messenger
action_fallback BackendWeb.FallbackController
action_fallback(BackendWeb.FallbackController)
@doc """
Given an instance, looks up the login types (email or admin account) and returns them. The user can then
@ -24,7 +24,7 @@ defmodule BackendWeb.AdminLoginController do
[error: "It is only possible to administer Mastodon and Pleroma instances."]
true ->
case get_and_decode("https://#{cleaned_domain}/api/v1/instance") do
case http_client().get_and_decode("https://#{cleaned_domain}/api/v1/instance") do
{:ok, instance_data} ->
[instance_data: instance_data, cleaned_domain: cleaned_domain]
@ -40,7 +40,7 @@ defmodule BackendWeb.AdminLoginController do
cleaned_domain = clean_domain(domain)
{data_state, instance_data} =
get_and_decode("https://#{cleaned_domain}/api/v1/instance",
http_client().get_and_decode("https://#{cleaned_domain}/api/v1/instance",
pool: :admin_login,
timeout: 20_000
)

Wyświetl plik

@ -1,6 +1,7 @@
defmodule BackendWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :backend
use Appsignal.Phoenix
plug BackendWeb.Healthcheck
socket("/socket", BackendWeb.UserSocket,
websocket: true,
@ -15,7 +16,7 @@ defmodule BackendWeb.Endpoint do
at: "/",
from: :backend,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
only: BackendWeb.static_paths()
)
# Code reloading can be explicitly enabled under the
@ -46,7 +47,11 @@ defmodule BackendWeb.Endpoint do
)
plug(Corsica,
origins: ["http://localhost:3000", ~r{^https://(.*\.?)fediverse\.space$}, ~r{^https://(.*\.?)fediverse-space\.netlify\.app$}],
origins: [
"http://localhost:3000",
~r{^https://(.*\.?)fediverse\.space$},
~r{^https://(.*\.?)fediverse-space\.netlify\.app$}
],
allow_headers: ["content-type", "token"]
)

Wyświetl plik

@ -0,0 +1,13 @@
defmodule BackendWeb.Healthcheck do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
conn
|> send_resp(200, "OK")
|> halt()
end
def call(conn, _opts), do: conn
end

Wyświetl plik

@ -11,7 +11,8 @@ defmodule BackendWeb.RateLimiter do
def rate_limit(conn, options \\ []) do
case check_rate(conn, options) do
{:ok, _count} -> conn # Do nothing, allow execution to continue
# Do nothing, allow execution to continue
{:ok, _count} -> conn
{:error, _count} -> render_error(conn)
end
end
@ -23,6 +24,7 @@ defmodule BackendWeb.RateLimiter do
else
Map.get(conn.params, "domain")
end
options = Keyword.put(options, :bucket_name, "authorization: #{domain}")
rate_limit(conn, options)
end
@ -40,7 +42,7 @@ defmodule BackendWeb.RateLimiter do
# "127.0.0.1:/api/v1/authorizations"
defp bucket_name(conn) do
path = Enum.join(conn.path_info, "/")
ip = conn.remote_ip |> Tuple.to_list |> Enum.join(".")
ip = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
"#{ip}:#{path}"
end
@ -48,6 +50,7 @@ defmodule BackendWeb.RateLimiter do
conn
|> put_status(:forbidden)
|> json(%{error: "Rate limit exceeded."})
|> halt # Stop execution of further plugs, return response now
# Stop execution of further plugs, return response now
|> halt
end
end

Wyświetl plik

@ -4,7 +4,8 @@ defmodule BackendWeb.Router do
pipeline :api do
plug(:accepts, ["json"])
plug(:rate_limit, max_requests: 5, interval_seconds: 10) # requests to the same endpoint
# requests to the same endpoint
plug(:rate_limit, max_requests: 5, interval_seconds: 10)
end
pipeline :api_admin do

Wyświetl plik

@ -28,7 +28,7 @@ defmodule Graph.Cache do
nodes = Api.list_nodes(domain)
edges = Api.list_edges(domain)
# Cache for 10 minutes
Cache.set(key, %{nodes: nodes, edges: edges}, ttl: 600)
Cache.put(key, %{nodes: nodes, edges: edges}, ttl: 600)
%{nodes: nodes, edges: edges}
data ->
@ -48,7 +48,7 @@ defmodule Graph.Cache do
Logger.debug("Instance cache: miss")
instance = Api.get_instance_with_relationships(domain)
# Cache for five minutes
Cache.set(key, instance, ttl: 300)
Cache.put(key, instance, ttl: 300)
instance
data ->
@ -82,7 +82,8 @@ defmodule Graph.Cache do
|> Repo.one()
# Cache for five minutes
Cache.set(key, crawl, ttl: 300)
Cache.put(key, crawl, ttl: 300)
crawl
data ->
Appsignal.increment_counter("most_recent_crawl_cache.hits", 1)

Wyświetl plik

@ -7,7 +7,6 @@ defmodule Backend.MixProject do
version: "2.8.2",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
@ -23,11 +22,10 @@ defmodule Backend.MixProject do
extra_applications: [
:logger,
:runtime_tools,
:mnesia,
:gollum,
:ex_twilio,
:elasticsearch,
:appsignal
:appsignal,
:swoosh,
:gen_smtp
]
]
end
@ -41,33 +39,41 @@ defmodule Backend.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.5"},
{:phoenix_pubsub, "~> 2.0"},
{:phoenix_ecto, "~> 4.0"},
{:phoenix_view, "~> 2.0"},
{:phoenix, "~> 1.7.0"},
{:phoenix_live_view, "~> 0.18.18"},
{:phoenix_live_dashboard, "~> 0.7.2"},
{:phoenix_html, "~> 3.0"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 0.5"},
{:phoenix_pubsub, "~> 2.1.1"},
{:phoenix_ecto, "~> 4.4.0"},
{:ecto_sql, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.1"},
{:httpoison, "~> 1.7", override: true},
{:httpoison, "~> 2.1", override: true},
{:timex, "~> 3.5"},
{:honeydew, "~> 1.4.3"},
{:honeydew, "~> 1.5.0"},
{:quantum, "~> 3.3"},
{:corsica, "~> 1.1.2"},
{:corsica, "~> 1.3"},
{:sobelow, "~> 0.8", only: [:dev, :test]},
{:gollum, "~> 0.3.2"},
{:public_suffix, git: "https://github.com/axelson/publicsuffix-elixir"},
{:swoosh, "~> 1.0"},
{:ex_twilio, "~> 0.8"},
{:gen_smtp, "~> 1.2"},
{:elasticsearch, "~> 1.0"},
{:appsignal, "~> 1.0"},
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
{:nebulex, "~> 1.1"},
{:appsignal, "~> 2.7"},
{:appsignal_phoenix, "~> 2.3"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:nebulex, "~> 2.4.2"},
{:hunter, "~> 0.5.1"},
{:scrivener_ecto, "~> 2.2"},
{:recase, "~> 0.7"},
{:ex_rated, "~> 2.0"},
{:html_sanitize_ex, "~> 1.4"}
{:ex_rated, "~> 2.1"},
{:html_sanitize_ex, "~> 1.4"},
{:mox, "~> 1.0", only: [:test]}
]
end

Wyświetl plik

@ -1,66 +1,81 @@
%{
"appsignal": {:hex, :appsignal, "1.13.4", "dee1f49ad0dbd60dae6cb6cc186a377e7b3a2fd4ea82340e534ef5f6835fb70b", [:make, :mix], [{:decorator, "~> 1.2.3 or ~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.9", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4659a8266c7dc86987571566a0cbc451ef0976b8491ab0106c8a5e31e98d8cd"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"appsignal": {:hex, :appsignal, "2.7.3", "5cd234052e49014c1590458627eebb68b9250ef13653734a0cf607736e05233b", [:make, :mix], [{:decorator, "~> 1.2.3 or ~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c24b96512ae7892b075d06d7294aadf89fb18b6e419ace6f3816783831e30d35"},
"appsignal_phoenix": {:hex, :appsignal_phoenix, "2.3.2", "80b2405fa8c4b8c27401b3dda32f90d380fb3f63d67efd6162d9489b8e409b06", [:mix], [{:appsignal, ">= 2.5.1 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:appsignal_plug, ">= 2.0.13 and < 3.0.0", [hex: :appsignal_plug, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.9", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8543c90688e309a503d7c3425558bf8182730b3efab546d84ec2f5c78eeaebfe"},
"appsignal_plug": {:hex, :appsignal_plug, "2.0.13", "daea31daec103248532c2facbe01098f53914ddecba47263a66574f3b322ac57", [:mix], [{:appsignal, ">= 2.5.1 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "704417abf28391ab3f6783ecf75b10242bf240554ddd8819d80b18b131cc2076"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8156b3a14a114a346262871333a931a1766b2597b56bf994fcfcb65443a348ad"},
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
"credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"},
"crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1347d889d1a0eda997990876b4894359e34bfbbd688acbb0ba28a2795ca40685"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"decorator": {:hex, :decorator, "1.3.2", "63b8ac9e23b28053390abdda33bb9e1f3dd9e8f9a981f47a06fc2f2fe2e2f772", [:mix], [], "hexpm", "b80bd089e3c8579e6d9ea84eed307b1597a0d94af25331e424a209477ad1a7fc"},
"ecto": {:hex, :ecto, "3.5.1", "c2c8ababbb36f12b2c5660ee2de538b58730557a2164b8907d1adff9b0b89991", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b5228da2678155f44abf01be27a59d9cf33709884d0c9f7858cf93e9460b0428"},
"ecto_sql": {:hex, :ecto_sql, "3.5.0", "760aa2935cc80b72da83fbd8cc97923623a2401915c308afea2cf2b0aabf4b2e", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3bab456e3ebb5680b327313f57ebb5356882a59fe04964a03232a83dc4c44aa2"},
"elasticsearch": {:hex, :elasticsearch, "1.0.0", "626d3fb8e7554d9c93eb18817ae2a3d22c2a4191cc903c4644b1334469b15374", [:mix], [{:httpoison, ">= 0.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sigaws, "~> 0.7", [hex: :sigaws, repo: "hexpm", optional: true]}, {:vex, "~> 0.6.0", [hex: :vex, repo: "hexpm", optional: false]}], "hexpm", "9fa0b717ad57a54c28451b3eb10c5121211c29a7b33615d2bcc7e2f3c9418b2e"},
"ex2ms": {:hex, :ex2ms, "1.6.0", "f39bbd9ff1b0f27b3f707bab2d167066dd8965e7df1149b962d94c74615d0e09", [:mix], [], "hexpm", "0d1ab5e08421af5cd69146efb408dbb1ff77f38a2f4df5f086f2512dc8cf65bf"},
"ex_rated": {:hex, :ex_rated, "2.0.0", "9772b1e5159d9f6067fb97da99fdc978be976605392e5e450692b2a5b3aab8ee", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "2468a791cb9daccaffc658fabd5b767f815be8c0ed118253931b77996419f443"},
"ex_twilio": {:hex, :ex_twilio, "0.8.2", "86c135827efc6e252a47d3fe95c37368959e840cd012900d11a3ded5a6ca554c", [:mix], [{:httpoison, ">= 0.9.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:poison, ">= 3.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "b14759966001eaa94d9d432822e9c245724197e4b53d0e40ef44d3f0052ef7bf"},
"gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm", "1d9fc978db5305ac54e6f5fec7adf80cd893b1000cf78271564c516aa2af7706"},
"gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"},
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"corsica": {:hex, :corsica, "1.3.0", "bbec02ccbeca1fdf44ee23b25a8ae32f7c6c28fc127ef8836dd8420e8f65bd9b", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8847ec817554047e9aa6d9933539cacb10c4ee60b58e0c15c3b380c5b737b35f"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"},
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
"elasticsearch": {:hex, :elasticsearch, "1.0.1", "8339538d90af6b280f10ecd02b1eae372f09373e629b336a13461babf7366495", [:mix], [{:httpoison, ">= 0.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sigaws, "~> 0.7", [hex: :sigaws, repo: "hexpm", optional: true]}, {:vex, "~> 0.6", [hex: :vex, repo: "hexpm", optional: false]}], "hexpm", "83e7d8b8bee3e7e19a06ab4d357d24845ac1da894e79678227fd52c0b7f71867"},
"ex2ms": {:hex, :ex2ms, "1.6.1", "66d472eb14da43087c156e0396bac3cc7176b4f24590a251db53f84e9a0f5f72", [:mix], [], "hexpm", "a7192899d84af03823a8ec2f306fa858cbcce2c2e7fd0f1c49e05168fb9c740e"},
"ex_rated": {:hex, :ex_rated, "2.1.0", "d40e6fe35097b10222df2db7bb5dd801d57211bac65f29063de5f201c2a6aebc", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "936c155337253ed6474f06d941999dd3a9cf0fe767ec99a59f2d2989dc2cc13f"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
"gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"},
"gollum": {:hex, :gollum, "0.3.3", "25ebb47700b9236bc4e5382bf91b72e4cdaf9bae3556172eff27e770735a198f", [:mix], [{:httpoison, "~> 1.5.1", [hex: :httpoison, repo: "hexpm", optional: false]}], "hexpm", "39268eeaf4f0adb6fdebe4f8c36b10a277881ab2eee3419c9b6727759e2f5a5d"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"honeydew": {:hex, :honeydew, "1.4.6", "1b13401df5186077e8958267a0a0e61e49068ec67c929739512977e2b867b71a", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "c6662f8e254a3c5e846ae67396d86f8c0a81801643683da87f7d501d7857e20b"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.1", "e8a67da405fe9f0d1be121a40a60f70811192033a5b8d00a95dddd807f5e053e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "68d92656f47cd73598c45ad2394561f025c8c65d146001b955fd7b517858962a"},
"httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"honeydew": {:hex, :honeydew, "1.5.0", "53088c1d87399efa5c0939adc8d32a9713b8fe6ce00a77c6769d2d363abac6bc", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "f71669e25f6a972e970ecbd79c34c4ad4b28369be78e4f8164fe8d0c5a674907"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.2", "c479398b6de798c03eb5d04a0a9a9159d73508f83f6590a00b8eacba3619cf4c", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "aef6c28585d06a9109ad591507e508854c5559561f950bbaea773900dd369b0e"},
"httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"},
"hunter": {:hex, :hunter, "0.5.1", "374dc4a800e2c340659657f8875e466075c7ea532e0d7a7787665f272b410150", [:mix], [{:httpoison, "~> 1.5", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "209b2cca7e4d51d5ff7ee4a0ab6cdc4c6ad23ddd61c9e12ceeee6f7ffbeae9c8"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"joken": {:hex, :joken, "2.3.0", "62a979c46f2c81dcb8ddc9150453b60d3757d1ac393c72bb20fc50a7b0827dc6", [:mix], [{:jose, "~> 1.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "57b263a79c0ec5d536ac02d569c01e6b4de91bd1cb825625fe90eab4feb7bc1e"},
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"libring": {:hex, :libring, "1.5.0", "44313eb6862f5c9168594a061e9d5f556a9819da7c6444706a9e2da533396d70", [:mix], [], "hexpm", "04e843d4fdcff49a62d8e03778d17c6cb2a03fe2d14020d3825a1761b55bd6cc"},
"jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm", "d1aeee7870470d2fa9eae0b3d5ab6c33801aa2d82b10e9dade885c5c921b36aa"},
"nebulex": {:hex, :nebulex, "1.2.2", "5b2bb7420a103b2a4278f354c9bd239bc77cd3bbdeddcebc4cc1d6ee656f126c", [:mix], [{:decorator, "~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:shards, "~> 0.6", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm", "6804ddd7660fd4010a5af5957316ab7471c2db003189dba79dc3dd7b3f0aabf6"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.5", "9a5a197edc1828c5f138a8ef10524dfecc43e36ab435c14578b1e9b4bd98858c", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b10eaf86ad026eafad2ee3dd336f0fb1c95a3711789855d913244e270bde463b"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"},
"plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"},
"plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
"mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"},
"mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"},
"nebulex": {:hex, :nebulex, "2.4.2", "b3d2d86d57b15896fb8e6d6dd49b4a9dee2eedd6eddfb3b69bfdb616a09c2817", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c9f888e5770fd47614c95990d0a02c3515216d51dc72e3c830eaf28f5649ba52"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.7.3", "4d8eca2c020c9ed81a28e7a8c60e0a4f6f9f7f6e12eb91dfd01301eac07424c1", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6b1bc308758f95ecf3e0d795389440a2ca88a903e0fda1f921c780918c16d640"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.2", "a4950b63ace57720b0fc1c6da083c53346b36f99021de89595cc4f026288ff51", [:mix], [], "hexpm", "45741676a94c71f9afdfed9d22d49b6856c026ff504db04e3dc03a1d86f8201c"},
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
"postgrex": {:hex, :postgrex, "0.15.6", "a464c72010a56e3214fe2b99c1a76faab4c2bb0255cabdef30dea763a3569aa2", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f99268325ac8f66ffd6c4964faab9e70fbf721234ab2ad238c00f9530b8cdd55"},
"postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"},
"public_suffix": {:git, "https://github.com/axelson/publicsuffix-elixir", "89372422ab8b433de508519ef474e39699fd11ca", []},
"quantum": {:hex, :quantum, "3.3.0", "e8f6b9479728774288c5f426b11a6e3e8f619f3c226163a7e18bccfe543b714d", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b83ef137ab3887e783b013418b5ce3e847d66b71c4ef0f233b0321c84b72f67"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"quantum": {:hex, :quantum, "3.5.0", "8d2c5ba68c55991e8975aca368e3ab844ba01f4b87c4185a7403280e2c99cf34", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "cab737d1d9779f43cb1d701f46dd05ea58146fd96238d91c9e0da662c1982bb6"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
"scrivener_ecto": {:hex, :scrivener_ecto, "2.5.0", "0fb56e243ce228b8f0f2b4ba36a370d8c21570a264a6f9fac3f322e2e114519f", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e3d2a57db3d5508b50dcb8a75d2a88b8e6360c050970a85917fc5e9c864ae7e9"},
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
"shards": {:hex, :shards, "0.6.2", "e05d05537883220c3b8a8f9d40d5c8ba7ff6064c63ebb6b23046972f6863b2d1", [:make, :rebar3], [], "hexpm", "58afa3712f1f1256a2a15e39fa95b7cd758087aaa7a25beaf786daabd87890f0"},
"sobelow": {:hex, :sobelow, "0.10.4", "44ba642da120d84fedb9e85473375084034330c8f15a992351dd164a82963103", [:mix], [], "hexpm", "fea62a94a4112de45ee9c9d076fd636fbbc10b7c7c2ea99a928e7c289b8498d1"},
"sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "94884f84783fc1ba027aba8fe8a7dae4aad78c98e9f9c76667ec3471585c08c6"},
"swoosh": {:hex, :swoosh, "1.0.5", "2c5b3c76304850b0a247f01f6b7570749052005af488028b9c99e3b2c5d978ad", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "86e5b2d0fd7e096b2223d6cdeec5940a2fe0a1557327eb155c05ee14b8a965f9"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
"tzdata": {:hex, :tzdata, "1.0.4", "a3baa4709ea8dba552dca165af6ae97c624a2d6ac14bd265165eaa8e8af94af6", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "b02637db3df1fd66dd2d3c4f194a81633d0e4b44308d36c1b2fdfd1e4e6f169b"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"vex": {:hex, :vex, "0.6.0", "4e79b396b2ec18cd909eed0450b19108d9631842598d46552dc05031100b7a56", [:mix], [], "hexpm", "7e4d9b50dd72cf931b52aba3470513686007f2ad54832de37cdb659cc85ba73e"},
"swoosh": {:hex, :swoosh, "1.11.0", "00b4fff8c08347a47cc5cbe67d64df5aae0607a7a51171944f5b89216e2d62f5", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5e7c49b6259e50a5ed756517e23a7f916c0b73eb0752e864b1d83b28e2c204d9"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
"telemetry_registry": {:hex, :telemetry_registry, "0.2.1", "fe648a691f2128e4279d993cd010994c67f282354dc061e697bf070d4b87b480", [:mix, :rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4221cefbcadd0b3e7076960339223742d973f1371bc20f3826af640257bc3690"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"vex": {:hex, :vex, "0.9.0", "613ea5eb3055662e7178b83e25b2df0975f68c3d8bb67c1645f0573e1a78d606", [:mix], [], "hexpm", "c69fff44d5c8aa3f1faee71bba1dcab05dd36364c5a629df8bb11751240c857f"},
"websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"},
"websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"},
}

6
backend/package-lock.json wygenerowano 100644
Wyświetl plik

@ -0,0 +1,6 @@
{
"name": "backend",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

Wyświetl plik

@ -0,0 +1,117 @@
defmodule Backend.Crawler.Crawlers.MastodonTest do
use Backend.DataCase
alias Backend.Crawler.Crawlers.Mastodon
alias Backend.Crawler.ApiCrawler
alias Backend.HttpBehaviour
import Mox
setup :verify_on_exit!
describe "is_instance_type?/2" do
test "returns true for pleroma and smithereen" do
assert Mastodon.is_instance_type?("example.com", %{instance_type: :pleroma})
assert Mastodon.is_instance_type?("example.com", %{instance_type: :smithereen})
end
test "returns true for mastodon instance" do
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance" ->
{:ok, TestHelpers.load_json("mastodon/instance.json")}
end)
assert Mastodon.is_instance_type?("example.com", nil)
end
end
describe "crawl/2" do
test "does nothing for small instances" do
expect(HttpMock, :get_and_decode!, fn "https://example.com/api/v1/instance" ->
TestHelpers.load_json("mastodon/instance.json")
|> Map.merge(%{"stats" => %{"user_count" => 1}})
end)
result = Mastodon.crawl("example.com", ApiCrawler.get_default())
assert result ==
ApiCrawler.get_default() |> Map.merge(%{instance_type: :mastodon, user_count: 1})
end
test "crawls large instance" do
expect(HttpMock, :get_and_decode!, fn "https://example.com/api/v1/instance" ->
TestHelpers.load_json("mastodon/instance.json")
end)
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance/peers" ->
{:ok, TestHelpers.load_json("mastodon/peers.json")}
end)
expect(
HttpMock,
:get_and_decode,
fn "https://example.com/api/v1/timelines/public?local=true&limit=40" ->
{:ok, TestHelpers.load_json("mastodon/timeline.json")}
end
)
expect(
HttpMock,
:get_and_decode,
4,
fn "https://example.com/api/v1/timelines/public?local=true&limit=40&max_id=123" ->
{:ok, TestHelpers.load_json("mastodon/timeline.json")}
end
)
result = Mastodon.crawl("example.com", ApiCrawler.get_default())
assert result == %{
description: "long description",
federation_restrictions: [],
instance_type: :mastodon,
interactions: %{},
peers: ["other.com"],
user_count: 100,
status_count: 100,
statuses_seen: 5,
version: "1.2.3"
}
end
test "handles timelines that require auth" do
expect(HttpMock, :get_and_decode!, fn "https://example.com/api/v1/instance" ->
TestHelpers.load_json("mastodon/instance.json")
end)
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance/peers" ->
{:ok, TestHelpers.load_json("mastodon/peers.json")}
end)
expect(
HttpMock,
:get_and_decode,
fn "https://example.com/api/v1/timelines/public?local=true&limit=40" ->
{:error,
%HttpBehaviour.Error{
message: "HTTP request failed with status code 422",
status_code: 422,
body: "{\"error\":\"This method requires an authenticated user\"}"
}}
end
)
result = Mastodon.crawl("example.com", ApiCrawler.get_default())
assert result == %{
description: "long description",
federation_restrictions: [],
instance_type: :mastodon,
interactions: %{},
peers: ["other.com"],
user_count: 100,
status_count: 100,
statuses_seen: 0,
version: "1.2.3"
}
end
end
end

Wyświetl plik

@ -0,0 +1,68 @@
defmodule Backend.Crawler.Crawlers.MisskeyTest do
use Backend.DataCase
alias Backend.Crawler.Crawlers.Misskey
alias Backend.Crawler.ApiCrawler
import Mox
setup :verify_on_exit!
describe "is_instance_type?/2" do
test "returns true for misskey instance" do
expect(HttpMock, :post_and_decode, fn "https://example.com/api/meta" ->
{:ok, TestHelpers.load_json("misskey/meta.json")}
end)
assert Misskey.is_instance_type?("example.com", nil)
end
end
describe "crawl/2" do
test "does nothing for small instances" do
expect(HttpMock, :post_and_decode, fn "https://example.com/api/stats" ->
stats =
TestHelpers.load_json("misskey/stats.json") |> Map.merge(%{"originalUsersCount" => 1})
{:ok, stats}
end)
result = Misskey.crawl("example.com", ApiCrawler.get_default())
assert result == ApiCrawler.get_default() |> Map.merge(%{type: :misskey, user_count: 1})
end
test "crawls large instances" do
expect(HttpMock, :post_and_decode, fn "https://example.com/api/stats" ->
{:ok, TestHelpers.load_json("misskey/stats.json")}
end)
expect(HttpMock, :post_and_decode, fn "https://example.com/api/meta" ->
{:ok, TestHelpers.load_json("misskey/meta.json")}
end)
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance/peers" ->
{:ok, TestHelpers.load_json("misskey/peers.json")}
end)
# status_count_limit is 5, response has 1 post per page, so we expect 5 requests
expect(HttpMock, :post_and_decode!, 5, fn "https://example.com/api/notes/local-timeline",
%{limit: 100} ->
TestHelpers.load_json("misskey/notes.json")
end)
result = Misskey.crawl("example.com", ApiCrawler.get_default())
assert result == %{
description: "some description",
federation_restrictions: [],
instance_type: :misskey,
interactions: %{},
peers: ["other.com"],
status_count: 20,
statuses_seen: 5,
user_count: 20,
version: "13.12.2"
}
end
end
end

Wyświetl plik

@ -0,0 +1,193 @@
defmodule Backend.Crawler.Crawlers.NodeinfoTest do
use ExUnit.Case
alias Backend.Crawler.Crawlers.Nodeinfo
import Mox
setup :verify_on_exit!
describe "crawl/2" do
test "handles valid nodeinfo" do
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
{:ok,
%{
"links" => [
%{
"rel" => "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href" => "https://mastodon.social/nodeinfo/2.0"
}
]
}}
end)
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/nodeinfo/2.0" ->
{:ok,
%{
"version" => "2.0",
"software" => %{
"name" => "Mastodon",
"version" => "1.2.3"
},
"protocols" => ["activitypub"],
"services" => %{
"inbound" => [],
"outbound" => []
},
"usage" => %{
"users" => %{
"total" => 100,
"activeMonth" => 1,
"activeHalfYear" => 2
},
"localPosts" => 3
},
"openRegistrations" => true,
"metadata" => %{}
}}
end)
result = Nodeinfo.crawl("mastodon.social", %{})
assert result == %{
description: nil,
user_count: 100,
status_count: 3,
statuses_seen: 0,
instance_type: :mastodon,
version: "1.2.3",
federation_restrictions: [],
interactions: %{},
peers: []
}
end
test "handles small instances" do
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
{:ok,
%{
"links" => [
%{
"rel" => "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href" => "https://mastodon.social/nodeinfo/2.0"
}
]
}}
end)
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/nodeinfo/2.0" ->
{:ok,
%{
"version" => "2.0",
"software" => %{
"name" => "Mastodon",
"version" => "1.2.3"
},
"protocols" => ["activitypub"],
"services" => %{
"inbound" => [],
"outbound" => []
},
"usage" => %{
"users" => %{
"total" => 1,
"activeMonth" => 1,
"activeHalfYear" => 1
},
"localPosts" => 3
},
"openRegistrations" => true,
"metadata" => %{}
}}
end)
result = Nodeinfo.crawl("mastodon.social", %{})
assert result == %{
description: nil,
user_count: 1,
status_count: nil,
statuses_seen: 0,
instance_type: nil,
version: nil,
federation_restrictions: [],
interactions: %{},
peers: []
}
end
test "handles missing nodeinfo" do
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
{:ok, %{}}
end)
result = Nodeinfo.crawl("mastodon.social", %{})
assert result == %{
description: nil,
user_count: nil,
status_count: nil,
statuses_seen: 0,
instance_type: nil,
version: nil,
federation_restrictions: [],
interactions: %{},
peers: []
}
end
test "handles non-200 response" do
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
{:error, %Backend.HttpBehaviour.Error{status_code: 401}}
end)
result = Nodeinfo.crawl("mastodon.social", %{})
assert result == %{
description: nil,
user_count: nil,
status_count: nil,
statuses_seen: 0,
instance_type: nil,
version: nil,
federation_restrictions: [],
interactions: %{},
peers: []
}
end
# don't know why some pixelfed instances return numbers as strings
# but i've seen it in the wild, so we need to handle it
test "handles nodeinfo with some numbers stringified (pixelfed)" do
expect(HttpMock, :get_and_decode, fn "https://pixelfed.social/.well-known/nodeinfo" ->
{:ok,
%{
"links" => [
%{
"rel" => "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href" => "https://pixelfed.social/nodeinfo/2.0.json"
}
]
}}
end)
expect(HttpMock, :get_and_decode, fn "https://pixelfed.social/nodeinfo/2.0.json" ->
{:ok, TestHelpers.load_json("nodeinfo/pixelfed.json")}
end)
result = Nodeinfo.crawl("pixelfed.social", %{})
assert result == %{
description:
"Pixelfed is an image sharing platform, an ethical alternative to centralized platforms.",
user_count: 16,
status_count: 60,
statuses_seen: 0,
instance_type: :pixelfed,
version: "0.11.2",
federation_restrictions: [],
interactions: %{},
peers: []
}
end
end
end

Wyświetl plik

@ -1,104 +0,0 @@
defmodule BackendWeb.GraphControllerTest do
use BackendWeb.ConnCase
alias Backend.Api
alias Backend.Api.Graph
@create_attrs %{
id: "some id",
label: "some label",
size: 120.5,
x: 120.5,
y: 120.5
}
@update_attrs %{
id: "some updated id",
label: "some updated label",
size: 456.7,
x: 456.7,
y: 456.7
}
@invalid_attrs %{id: nil, label: nil, size: nil, x: nil, y: nil}
def fixture(:graph) do
{:ok, graph} = Api.create_graph(@create_attrs)
graph
end
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index" do
test "lists all nodes", %{conn: conn} do
conn = get(conn, Routes.graph_path(conn, :index))
assert json_response(conn, 200)["data"] == []
end
end
describe "create graph" do
test "renders graph when data is valid", %{conn: conn} do
conn = post(conn, Routes.graph_path(conn, :create), graph: @create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, Routes.graph_path(conn, :show, id))
assert %{
"id" => id,
"id" => "some id",
"label" => "some label",
"size" => 120.5,
"x" => 120.5,
"y" => 120.5
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, Routes.graph_path(conn, :create), graph: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "update graph" do
setup [:create_graph]
test "renders graph when data is valid", %{conn: conn, graph: %Graph{id: id} = graph} do
conn = put(conn, Routes.graph_path(conn, :update, graph), graph: @update_attrs)
assert %{"id" => ^id} = json_response(conn, 200)["data"]
conn = get(conn, Routes.graph_path(conn, :show, id))
assert %{
"id" => id,
"id" => "some updated id",
"label" => "some updated label",
"size" => 456.7,
"x" => 456.7,
"y" => 456.7
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn, graph: graph} do
conn = put(conn, Routes.graph_path(conn, :update, graph), graph: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "delete graph" do
setup [:create_graph]
test "deletes chosen graph", %{conn: conn, graph: graph} do
conn = delete(conn, Routes.graph_path(conn, :delete, graph))
assert response(conn, 204)
assert_error_sent 404, fn ->
get(conn, Routes.graph_path(conn, :show, graph))
end
end
end
defp create_graph(_) do
graph = fixture(:graph)
{:ok, graph: graph}
end
end

Wyświetl plik

@ -1,91 +0,0 @@
defmodule BackendWeb.InstanceControllerTest do
use BackendWeb.ConnCase
alias Backend.Api
alias Backend.Api.Instance
@create_attrs %{
name: "some name"
}
@update_attrs %{
name: "some updated name"
}
@invalid_attrs %{name: nil}
def fixture(:instance) do
{:ok, instance} = Api.create_instance(@create_attrs)
instance
end
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index" do
test "lists all instances", %{conn: conn} do
conn = get(conn, Routes.instance_path(conn, :index))
assert json_response(conn, 200)["data"] == []
end
end
describe "create instance" do
test "renders instance when data is valid", %{conn: conn} do
conn = post(conn, Routes.instance_path(conn, :create), instance: @create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, Routes.instance_path(conn, :show, id))
assert %{
"id" => id,
"name" => "some name"
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, Routes.instance_path(conn, :create), instance: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "update instance" do
setup [:create_instance]
test "renders instance when data is valid", %{
conn: conn,
instance: %Instance{id: id} = instance
} do
conn = put(conn, Routes.instance_path(conn, :update, instance), instance: @update_attrs)
assert %{"id" => ^id} = json_response(conn, 200)["data"]
conn = get(conn, Routes.instance_path(conn, :show, id))
assert %{
"id" => id,
"name" => "some updated name"
} = json_response(conn, 200)["data"]
end
test "renders errors when data is invalid", %{conn: conn, instance: instance} do
conn = put(conn, Routes.instance_path(conn, :update, instance), instance: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "delete instance" do
setup [:create_instance]
test "deletes chosen instance", %{conn: conn, instance: instance} do
conn = delete(conn, Routes.instance_path(conn, :delete, instance))
assert response(conn, 204)
assert_error_sent 404, fn ->
get(conn, Routes.instance_path(conn, :show, instance))
end
end
end
defp create_instance(_) do
instance = fixture(:instance)
{:ok, instance: instance}
end
end

Wyświetl plik

@ -19,11 +19,14 @@ defmodule BackendWeb.ConnCase do
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
import Plug.Conn
import Phoenix.ConnTest
alias BackendWeb.Router.Helpers, as: Routes
# The default endpoint for testing
@endpoint BackendWeb.Endpoint
use BackendWeb, :verified_routes
end
end

Wyświetl plik

@ -0,0 +1,137 @@
{
"uri": "mastodon.social",
"title": "Mastodon",
"short_description": "short description",
"description": "long description",
"email": "staff@mastodon.social",
"version": "1.2.3",
"urls": {
"streaming_api": "wss://streaming.mastodon.social"
},
"stats": {
"user_count": 100,
"status_count": 100,
"domain_count": 55958
},
"thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
"languages": ["en"],
"registrations": true,
"approval_required": false,
"invites_enabled": true,
"configuration": {
"accounts": {
"max_featured_tags": 10
},
"statuses": {
"max_characters": 500,
"max_media_attachments": 4,
"characters_reserved_per_url": 23
},
"media_attachments": {
"supported_mime_types": [
"image/jpeg",
"image/png",
"image/gif",
"image/heic",
"image/heif",
"image/webp",
"image/avif",
"video/webm",
"video/mp4",
"video/quicktime",
"video/ogg",
"audio/wave",
"audio/wav",
"audio/x-wav",
"audio/x-pn-wave",
"audio/vnd.wave",
"audio/ogg",
"audio/vorbis",
"audio/mpeg",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/aac",
"audio/m4a",
"audio/x-m4a",
"audio/mp4",
"audio/3gpp",
"video/x-ms-asf"
],
"image_size_limit": 16777216,
"image_matrix_limit": 33177600,
"video_size_limit": 103809024,
"video_frame_rate_limit": 120,
"video_matrix_limit": 8294400
},
"polls": {
"max_options": 4,
"max_characters_per_option": 50,
"min_expiration": 300,
"max_expiration": 2629746
}
},
"contact_account": {
"id": "13179",
"username": "Mastodon",
"acct": "Mastodon",
"display_name": "Mastodon",
"locked": false,
"bot": false,
"discoverable": true,
"group": false,
"created_at": "2016-11-23T00:00:00.000Z",
"note": "<p>Official account of the Mastodon project. News, releases, announcements! Learn more on our website!</p>",
"url": "https://mastodon.social/@Mastodon",
"avatar": "https://files.mastodon.social/accounts/avatars/000/013/179/original/b4ceb19c9c54ec7e.png",
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/013/179/original/b4ceb19c9c54ec7e.png",
"header": "https://files.mastodon.social/accounts/headers/000/013/179/original/878f382e7dd9fb84.png",
"header_static": "https://files.mastodon.social/accounts/headers/000/013/179/original/878f382e7dd9fb84.png",
"followers_count": 778859,
"following_count": 8,
"statuses_count": 237,
"last_status_at": "2023-05-13",
"noindex": false,
"emojis": [],
"roles": [],
"fields": [
{
"name": "Homepage",
"value": "<a href=\"https://joinmastodon.org\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">joinmastodon.org</span><span class=\"invisible\"></span></a>",
"verified_at": "2018-10-31T04:11:00.076+00:00"
},
{
"name": "Patreon",
"value": "<a href=\"https://patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>",
"verified_at": null
},
{
"name": "GitHub",
"value": "<a href=\"https://github.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">github.com/mastodon</span><span class=\"invisible\"></span></a>",
"verified_at": null
}
]
},
"rules": [
{
"id": "1",
"text": "Sexually explicit or violent media must be marked as sensitive when posting"
},
{
"id": "2",
"text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
},
{
"id": "3",
"text": "No incitement of violence or promotion of violent ideologies"
},
{
"id": "4",
"text": "No harassment, dogpiling or doxxing of other users"
},
{
"id": "7",
"text": "Do not share intentionally false or misleading information"
}
]
}

Wyświetl plik

@ -0,0 +1 @@
["other.com"]

Wyświetl plik

@ -0,0 +1,55 @@
[
{
"id": "123",
"created_at": "2023-06-10T18:59:36.207Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"sensitive": false,
"spoiler_text": "",
"visibility": "public",
"language": "de",
"uri": "https://mastodon.social/users/someuser/statuses/110521455489577427",
"url": "https://mastodon.social/@someuser/110521455489577427",
"replies_count": 0,
"reblogs_count": 0,
"favourites_count": 0,
"edited_at": null,
"content": "<p>New post</p>",
"reblog": null,
"application": {
"name": "IFTTT",
"website": "https://www.ifttt.com"
},
"account": {
"id": "108265572384945996",
"username": "someuser",
"acct": "someuser",
"display_name": "Some User",
"locked": false,
"bot": false,
"discoverable": true,
"group": false,
"created_at": "2022-05-08T00:00:00.000Z",
"note": "<p>My account</p>",
"url": "https://mastodon.social/@someuser",
"avatar": "https://example.com/picture.jpg",
"avatar_static": "https://example.com/picture.jpg",
"header": "https://example.com/picture.jpg",
"header_static": "https://example.com/picture.jpg",
"followers_count": 7,
"following_count": 73,
"statuses_count": 256,
"last_status_at": "2023-06-10",
"noindex": false,
"emojis": [],
"roles": [],
"fields": []
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": {},
"poll": null
}
]

Wyświetl plik

@ -0,0 +1,170 @@
{
"maintainerName": "MisskeyHQ",
"maintainerEmail": "https://go.misskey.io/support",
"version": "13.12.2",
"name": "Misskey.io",
"uri": "https://misskey.io",
"description": "some description",
"langs": ["ja", "en", "zh", "ko", "fr", "de"],
"tosUrl": "http://go.misskey.io/tos",
"repositoryUrl": "https://github.com/syuilo/misskey",
"feedbackUrl": "https://github.com/syuilo/misskey/issues/new",
"disableRegistration": false,
"emailRequiredForSignup": true,
"enableHcaptcha": false,
"hcaptchaSiteKey": "95d75440-7e37-4419-a693-8f52c377f1c5",
"enableRecaptcha": false,
"recaptchaSiteKey": "6LfW8qQUAAAAAI_1WMThmcj6zO39laasAoEJHfFF",
"enableTurnstile": true,
"turnstileSiteKey": "0x4AAAAAAACJmZyh3LCvo-uf",
"swPublickey": "BHqCPVsCM8pMUo26Fenl6fuLPfuqQTNeo2Rpvt6KFxFEKznKAXZBHI2nk1aAanlJ1Me_PSr-MVkW3ho4RaYmZpk",
"themeColor": "#86b300",
"mascotImageUrl": "/assets/ai.png",
"bannerUrl": "https://s3.arkjp.net/misskey/65b25d3c-2ae4-474f-b1c0-050c8c8962e1.jpg",
"errorImageUrl": "https://s3.arkjp.net/misskey/94aab3c5-0b26-42a7-9fa9-83a69d7253cd.png",
"iconUrl": "https://s3.arkjp.net/misskey/webpublic-0c66b1ca-b8c0-4eaa-9827-47674f4a1580.png",
"backgroundImageUrl": "https://s3.arkjp.net/misskey/e23f6837-c477-4f40-bbc7-b8a06e3bc1cc.jpg",
"logoImageUrl": "https://s3.arkjp.net/misskey/31240fa8-98fa-4750-bfd4-767753d1c48d.png",
"maxNoteTextLength": 3000,
"defaultLightTheme": null,
"defaultDarkTheme": null,
"ads": [
{
"id": "8riz9d7mt0",
"url": "http://go.misskey.io/nextdns",
"place": "horizontal",
"ratio": 3,
"imageUrl": "https://s3.arkjp.net/misskey/03992473-790d-4e50-9f70-f12ed1a5aabb.png"
},
{
"id": "8rkte84ghf",
"url": "https://go.misskey.io/vultr",
"place": "horizontal",
"ratio": 3,
"imageUrl": "https://s3.arkjp.net/misskey/fa1421c3-fabc-4dbc-a688-52dfb7491660.webp"
},
{
"id": "97crkngnt7",
"url": "https://go.misskey.io/ads",
"place": "horizontal",
"ratio": 1,
"imageUrl": "https://s3.arkjp.net/misskey/d85e0e31-522b-4779-8479-b7acb65c86dc.png"
},
{
"id": "97crxz63al",
"url": "https://go.misskey.io/ads",
"place": "horizontal",
"ratio": 1,
"imageUrl": "https://s3.arkjp.net/misskey/d73e0b21-5910-42f7-9f1b-6983026ee1db.png"
},
{
"id": "97cspskh3f",
"url": "https://go.misskey.io/ads",
"place": "horizontal",
"ratio": 1,
"imageUrl": "https://s3.arkjp.net/misskey/36fe91e6-5d11-4f12-8bc8-426ab0ebd885.png"
},
{
"id": "9clihjru6p",
"url": "https://go.misskey.io/maZC",
"place": "horizontal",
"ratio": 40,
"imageUrl": "https://s3.arkjp.net/misskey/ee74935a-a1ec-4385-bb36-2377387118b8.png"
},
{
"id": "9e5idmskrv",
"url": "https://go.misskey.io/LfNP",
"place": "horizontal-big",
"ratio": 20,
"imageUrl": "https://s3.arkjp.net/misskey/4e03fca5-b25f-4950-974a-313a7d958b6d.png"
},
{
"id": "9eo9im87s6",
"url": "https://go.misskey.io/jHVi",
"place": "horizontal-big",
"ratio": 40,
"imageUrl": "https://s3.arkjp.net/misskey/7e903951-e7e1-4277-badf-0ea5bc9ab07a.png"
},
{
"id": "9f00tkswhg",
"url": "https://go.misskey.io/wD6e",
"place": "horizontal-big",
"ratio": 20,
"imageUrl": "https://s3.arkjp.net/misskey/015a3335-b21e-4897-a958-d6879b2a82f1.png"
},
{
"id": "9f3stos3s7",
"url": "https://go.misskey.io/iMxB",
"place": "horizontal-big",
"ratio": 40,
"imageUrl": "https://s3.arkjp.net/misskey/098ceb69-8238-4c3c-8f99-f9752294cb96.png"
},
{
"id": "9fgxdm8mpd",
"url": "https://go.misskey.io/dwRP",
"place": "horizontal-big",
"ratio": 60,
"imageUrl": "https://s3.arkjp.net/misskey/c4650ddc-687e-46c0-932a-c1f5ca8c9f83.png"
},
{
"id": "9fhdbdyevw",
"url": "https://go.misskey.io/pwYv",
"place": "horizontal",
"ratio": 40,
"imageUrl": "https://s3.arkjp.net/misskey/e2f6c692-0d16-4fe9-90c4-25eac1b31731.png"
},
{
"id": "9fgjjfdr3s",
"url": "https://go.misskey.io/VwEm",
"place": "horizontal-big",
"ratio": 60,
"imageUrl": "https://s3.arkjp.net/misskey/951bda53-3707-480e-ab5a-0000ca9c7578.png"
},
{
"id": "9fnpd9tsgs",
"url": "https://go.misskey.io/QwId",
"place": "horizontal-big",
"ratio": 80,
"imageUrl": "https://s3.arkjp.net/misskey/261f7323-7a5e-4734-92bc-6ad69a4226df.jpg"
},
{
"id": "9fmru4ok7f",
"url": "https://go.misskey.io/pwYv",
"place": "horizontal-big",
"ratio": 20,
"imageUrl": "https://s3.arkjp.net/misskey/563a709e-6d5e-4952-9cb5-ac6897f80990.png"
},
{
"id": "9ew2fhwyfc",
"url": "https://go.misskey.io/sjxJ",
"place": "horizontal-big",
"ratio": 60,
"imageUrl": "https://s3.arkjp.net/misskey/8dff6f2d-444f-459f-80ff-02cad454be91.png"
}
],
"enableEmail": true,
"enableServiceWorker": true,
"translatorAvailable": true,
"serverRules": [],
"policies": {
"gtlAvailable": true,
"ltlAvailable": true,
"canPublicNote": true,
"canInvite": false,
"canManageCustomEmojis": false,
"canSearchNotes": false,
"canHideAds": false,
"driveCapacityMb": 10240,
"alwaysMarkNsfw": false,
"pinLimit": 3,
"antennaLimit": 5,
"wordMuteLimit": 200,
"webhookLimit": 3,
"clipLimit": 10,
"noteEachClipsLimit": 50,
"userListLimit": 5,
"userEachUserListsLimit": 20,
"rateLimitFactor": 2
},
"mediaProxy": "https://nos3.arkjp.net"
}

Wyświetl plik

@ -0,0 +1,33 @@
[
{
"id": "9ftvwz5kx8no7aeb",
"createdAt": "2023-06-10T18:18:57.656Z",
"userId": "9badbj0vp9",
"user": {
"id": "9badbj0vp9",
"name": "Some Name",
"username": "username",
"host": null,
"avatarUrl": "https://example.com/image.png",
"avatarBlurhash": "foobar",
"avatarColor": null,
"speakAsCat": true,
"emojis": [],
"onlineStatus": "online",
"driveCapacityOverrideMb": null
},
"text": "My post",
"cw": null,
"visibility": "public",
"renoteCount": 0,
"repliesCount": 0,
"reactions": {},
"reactionEmojis": [],
"emojis": [],
"tags": ["post"],
"fileIds": [],
"files": [],
"replyId": null,
"renoteId": null
}
]

Wyświetl plik

@ -0,0 +1 @@
["other.com"]

Wyświetl plik

@ -0,0 +1,10 @@
{
"notesCount": 10,
"originalNotesCount": 20,
"usersCount": 10,
"originalUsersCount": 20,
"reactionsCount": 64569657,
"instances": 21184,
"driveUsageLocal": 0,
"driveUsageRemote": 0
}

Wyświetl plik

@ -0,0 +1,89 @@
{
"metadata": {
"nodeName": "Pixelfed",
"software": {
"homepage": "https://pixelfed.org",
"repo": "https://github.com/pixelfed/pixelfed"
},
"config": {
"open_registration": true,
"uploader": {
"max_photo_size": "15000",
"max_caption_length": "500",
"album_limit": "4",
"image_quality": 80,
"max_collection_length": 18,
"optimize_image": true,
"optimize_video": true,
"media_types": "image/jpeg,image/png,image/gif",
"enforce_account_limit": true
},
"activitypub": {
"enabled": true,
"remote_follow": true
},
"ab": {
"lc": false,
"rec": false,
"loops": false,
"top": false,
"polls": false,
"cached_public_timeline": false,
"gps": false,
"spa": true,
"emc": false
},
"site": {
"name": "Pixelfe",
"domain": "pixelfed.example.com",
"url": "https://pixelfed.example.com",
"description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms."
},
"username": {
"remote": {
"formats": ["@", "from", "custom"],
"format": "@",
"custom": null
}
},
"features": {
"mobile_apis": true,
"circles": false,
"stories": false,
"video": false,
"import": {
"instagram": false,
"mastodon": false,
"pixelfed": false
},
"label": {
"covid": {
"enabled": true,
"org": "visit the WHO website",
"url": "https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public"
}
}
}
}
},
"protocols": ["activitypub"],
"services": {
"inbound": [],
"outbound": []
},
"software": {
"name": "pixelfed",
"version": "0.11.2"
},
"usage": {
"localPosts": "60",
"localComments": 0,
"users": {
"total": "16",
"activeHalfyear": 16,
"activeMonth": 2
}
},
"version": "2.0",
"openRegistrations": true
}

Wyświetl plik

@ -1,2 +1,14 @@
Mox.defmock(HttpMock, for: Backend.HttpBehaviour)
Application.put_env(:backend, :http, HttpMock)
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Backend.Repo, :manual)
defmodule TestHelpers do
@spec load_json(String.t()) :: any()
def load_json(path) do
Path.join([__DIR__, "support", "data", "json", path])
|> File.read!()
|> Jason.decode!()
end
end

Wyświetl plik

@ -0,0 +1,36 @@
version: "2"
networks:
space:
external: false
services:
phoenix:
build: backend
restart: unless-stopped
networks:
- space
depends_on:
- db
gephi:
build: gephi
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
networks:
- space
volumes:
- /var/lib/postgresql/data
elastic:
image: elasticsearch:8.7.0
restart: unless-stopped
environment:
- discovery.type=single-node
networks:
- space

Wyświetl plik

@ -5,20 +5,30 @@ module.exports = {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
},
plugins: ["@typescript-eslint", "prettier"],
plugins: ["@typescript-eslint"],
extends: [
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier/@typescript-eslint",
"prettier",
],
rules: {
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"react/prop-types": 0,
"@typescript-eslint/no-non-null-assertion": 0
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-unsafe-assignment": ["off"],
"@typescript-eslint/no-unsafe-argument": ["off"],
"@typescript-eslint/no-unsafe-call": ["off"],
"@typescript-eslint/no-unsafe-member-access": ["off"],
"@typescript-eslint/no-unsafe-return": ["off"],
"@typescript-eslint/restrict-template-expressions": ["off"],
},
settings: {
react: {
version: "detect",
},
},
};

Wyświetl plik

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
cd frontend
npx lint-staged

Plik diff jest za duży Load Diff

Plik diff jest za duży Load Diff

Wyświetl plik

@ -11,7 +11,7 @@
<!-- Open Graph -->
<meta property="og:site_name" content="fediverse.space" />
<meta property="og:description" content="" />
<meta property="og:image" content="%PUBLIC_URL%/preview.png" />
<meta property="og:image" content="/preview.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="914" />
<meta property="og:image:height" content="679" />
@ -20,25 +20,13 @@
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="fediverse.space">
<meta name="twitter:description" content="A tool to visualize decentralized social networks.">
<meta name="twitter:image" content="%PUBLIC_URL%/preview.png">
<meta name="twitter:image" content="/preview.png">
<meta name="twitter:image:alt" content="A screenshot of fediverse.space. Shows a graph of fediverse instances." />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="shortcut icon" href="/favicon.ico">
<title>fediverse.space</title>
<script defer data-domain="fediverse.space" data-api="https://btao.org/workers/btao/event" src="https://btao.org/workers/btao/script.js"></script>
</head>
<body>
@ -46,6 +34,8 @@
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<a rel="me" href="https://mastodon.social/@fediversespace" style="display: none">Mastodon</a>
</body>
</html>

7071
frontend/package-lock.json wygenerowano 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -3,94 +3,84 @@
"version": "2.8.2",
"private": true,
"scripts": {
"start": "NODE_ENV=development react-scripts start",
"build": "react-scripts build",
"start": "vite",
"build": "npm run typecheck && vite build",
"serve": "vite preview",
"typecheck": "tsc --noemit",
"lint": "yarn typecheck && yarn eslint src/ --ext .js,.jsx,.ts,.tsx",
"lint:fix": "yarn lint --fix",
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
"lint:fix": "npm run lint --fix",
"pretty": "prettier --write \"src/**/*.{ts,tsx}\"",
"test": "yarn lint && react-scripts test --ci",
"eject": "react-scripts eject",
"snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
"prepare": "cd .. && husky install frontend/.husky"
},
"lint-staged": {
"src/**/*.{ts,tsx}": [
"yarn pretty",
"yarn lint:fix"
"npm run pretty",
"npm run lint:fix"
]
},
"prettier": {
"printWidth": 120
},
"dependencies": {
"@blueprintjs/core": "^3.33.0",
"@blueprintjs/icons": "^3.22.0",
"@blueprintjs/select": "^3.14.2",
"classnames": "^2.2.6",
"connected-react-router": "^6.5.2",
"cross-fetch": "^3.0.6",
"cytoscape": "^3.16.1",
"cytoscape-popper": "^1.0.7",
"inflection": "^1.12.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"normalize.css": "^8.0.0",
"@blueprintjs/core": "^4.20.1",
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/select": "^4.9.22",
"classnames": "^2.3.2",
"connected-react-router": "^6.9.3",
"cross-fetch": "^3.1.6",
"cytoscape": "^3.25.0",
"cytoscape-popper": "^2.0.0",
"inflection": "^2.0.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"normalize.css": "^8.0.1",
"numeral": "^2.0.6",
"react": "^16.10.2",
"react-dom": "^16.10.2",
"react-redux": "^7.2.1",
"react-router-dom": "^5.2.0",
"react-sigma": "^1.2.35",
"react-virtualized": "^9.22.2",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"sanitize-html": "^2.0.0",
"snyk": "^1.410.1",
"styled-components": "^5.2.0",
"tippy.js": "^4.3.5"
"react": "^17",
"react-dom": "^17",
"react-redux": "^7",
"react-router-dom": "^5",
"react-virtualized": "^9.22.5",
"redux": "^4.2.1",
"redux-first-history": "^5.1.1",
"redux-thunk": "^2.4.2",
"sanitize-html": "^2.10.0",
"styled-components": "^5.3.11",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/cytoscape": "^3.14.7",
"@types/inflection": "^1.5.28",
"@types/jest": "^26.0.14",
"@types/lodash": "^4.14.161",
"@types/node": "^14.11.5",
"@types/numeral": "^0.0.28",
"@types/react": "^16.9.51",
"@types/react-axe": "^3.1.0",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.6",
"@types/sanitize-html": "^1.27.0",
"@types/styled-components": "5.1.3",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.34.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.3",
"eslint-plugin-react-hooks": "^4.1.2",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2",
"react-axe": "3.5.3",
"react-scripts": "3.4.3",
"typescript": "^3.9.2"
"@types/classnames": "^2.3.0",
"@types/cytoscape": "^3.19.9",
"@types/inflection": "^1.13.0",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.14.195",
"@types/node": "^20.2.6",
"@types/numeral": "^2.0.2",
"@types/react": "^17",
"@types/react-dom": "^17",
"@types/react-redux": "^7.1.25",
"@types/react-router-dom": "^5.3.3",
"@types/sanitize-html": "^2.9.0",
"@types/styled-components": "5.1.26",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"prettier": "^2.8.8",
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vite-plugin-svgr": "^3.2.0"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"snyk": true
]
}

Wyświetl plik

@ -1,15 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

Wyświetl plik

@ -3,7 +3,7 @@ import React from "react";
import { Classes } from "@blueprintjs/core";
import { ConnectedRouter } from "connected-react-router";
import { Route } from "react-router-dom";
import { Route, Switch } from "react-router-dom";
import { Nav } from "./components/organisms";
import {
AboutScreen,
@ -20,11 +20,23 @@ const AppRouter: React.FC = () => (
<div className={`${Classes.DARK} App`}>
<Nav />
<main role="main">
<Route path="/instances" exact component={TableScreen} />
<Route path="/about" exact component={AboutScreen} />
<Route path="/admin/login" exact component={LoginScreen} />
<Route path="/admin/verify" exact component={VerifyLoginScreen} />
<Route path="/admin" exact component={AdminScreen} />
<Switch>
<Route path="/instances" exact>
<TableScreen />
</Route>
<Route path="/about" exact>
<AboutScreen />
</Route>
<Route path="/admin/login" exact>
<LoginScreen />
</Route>
<Route path="/admin/verify" exact>
<VerifyLoginScreen />
</Route>
<Route path="/admin" exact>
<AdminScreen />
</Route>
</Switch>
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
<GraphScreen />
</main>

Wyświetl plik

@ -3,7 +3,7 @@ import { isEqual } from "lodash";
import * as React from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import tippy, { Instance } from "tippy.js";
import tippy from "tippy.js";
import {
DEFAULT_NODE_COLOR,
HOVERED_NODE_COLOR,
@ -65,10 +65,10 @@ class Cytoscape extends React.PureComponent<CytoscapeProps> {
trigger: "manual",
});
n.on("mouseover", () => {
(t as Instance).show();
(t as any).show();
});
n.on("mouseout", () => {
(t as Instance).hide();
(t as any).hide();
});
});

Wyświetl plik

@ -13,7 +13,7 @@ interface NavState {
const graphIsActive = (currMatch: match<InstanceDomainPath>, location: Location) =>
location.pathname === "/" || location.pathname.startsWith("/instance/");
class Nav extends React.Component<{}, NavState> {
class Nav extends React.Component<Record<string, never>, NavState> {
constructor(props: any) {
super(props);
this.state = { aboutIsOpen: false };

Wyświetl plik

@ -1,9 +1,9 @@
import { Classes, Code, H1, H2, H3 } from "@blueprintjs/core";
import * as React from "react";
import styled from "styled-components";
import * as appsignalLogo from "../../assets/appsignal.png";
import * as gitlabLogo from "../../assets/gitlab.png";
import * as nlnetLogo from "../../assets/nlnet.png";
import appsignalLogo from "../../assets/appsignal.png";
import gitlabLogo from "../../assets/gitlab.png";
import nlnetLogo from "../../assets/nlnet.png";
import { Page } from "../atoms";
const SponsorContainer = styled.div`

Wyświetl plik

@ -3,13 +3,15 @@ import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
import { Route, Switch } from "react-router";
import { InstanceScreen, SearchScreen } from ".";
import { INSTANCE_DOMAIN_PATH } from "../../constants";
import { loadInstance } from "../../redux/actions";
import { AppState } from "../../redux/types";
import { domainMatchSelector, isSmallScreen } from "../../util";
import { Graph, SidebarContainer } from "../organisms";
import { useLocation } from "react-router-dom";
import type { Location } from "history";
const GraphContainer = styled.div`
display: flex;
@ -24,7 +26,8 @@ const FullDiv = styled.div`
right: 0;
`;
interface GraphScreenProps extends RouteComponentProps {
interface GraphScreenProps {
location: Location;
currentInstanceName: string | null;
pathname: string;
graphLoadError: boolean;
@ -79,8 +82,12 @@ class GraphScreenImpl extends React.Component<GraphScreenProps, GraphScreenState
{isSmallScreen || !this.state.hasBeenViewed || <Graph />}
<SidebarContainer>
<Switch>
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
<Route exact path="/" component={SearchScreen} />
<Route path={INSTANCE_DOMAIN_PATH}>
<InstanceScreen />
</Route>
<Route exact path="/">
<SearchScreen />
</Route>
</Switch>
</SidebarContainer>
</GraphContainer>
@ -106,4 +113,9 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any),
});
const GraphScreen = connect(mapStateToProps, mapDispatchToProps)(GraphScreenImpl);
export default withRouter(GraphScreen);
const Component = (props: Omit<React.ComponentProps<typeof GraphScreen>, "location">) => {
const location = useLocation();
return <GraphScreen {...props} location={location} />;
};
Component.displayName = "GraphScreen";
export default Component;

Wyświetl plik

@ -15,6 +15,7 @@ import {
H2,
HTMLTable,
Icon,
IconSize,
NonIdealState,
Position,
Spinner,
@ -233,11 +234,11 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
</StyledTabs>
<StyledLinkToFdNetwork>
<AnchorButton
href={`https://fediverse.network/${this.props.instanceName}`}
href={`https://fedidb.org/network/instance?domain=${this.props.instanceName}`}
minimal
rightIcon={IconNames.SHARE}
target="_blank"
text="See more statistics at fediverse.network"
text="See more statistics at fedidb.org"
/>
</StyledLinkToFdNetwork>
</>
@ -296,7 +297,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
statusesPerUserPerDay,
} = this.props.instanceDetails;
return (
<StyledHTMLTable small striped>
<StyledHTMLTable condensed striped>
<tbody>
<tr>
<td>Version</td>
@ -329,7 +330,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
position={Position.TOP}
className={Classes.DARK}
>
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
<Icon icon={IconNames.HELP} iconSize={IconSize.STANDARD} />
</Tooltip>
</td>
<td>{(insularity && numeral.default(insularity).format("0.0%")) || "Unknown"}</td>
@ -349,7 +350,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
position={Position.TOP}
className={Classes.DARK}
>
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
<Icon icon={IconNames.HELP} iconSize={IconSize.STANDARD} />
</Tooltip>
</td>
<td>{(statusesPerDay && numeral.default(statusesPerDay).format("0.0")) || "Unknown"}</td>
@ -369,7 +370,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
position={Position.TOP}
className={Classes.DARK}
>
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
<Icon icon={IconNames.HELP} iconSize={IconSize.STANDARD} />
</Tooltip>
</td>
<td>{(statusesPerUserPerDay && numeral.default(statusesPerUserPerDay).format("0.000")) || "Unknown"}</td>
@ -420,7 +421,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
would mean that every single status on {this.props.instanceName} contained a mention of someone on the other
instance, and vice versa.
</p>
<StyledHTMLTable small striped interactive={false}>
<StyledHTMLTable condensed striped interactive={false}>
<thead>
<tr>
<th>Instance</th>
@ -456,7 +457,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
<p className={Classes.TEXT_MUTED}>
All the instances, past and present, that {this.props.instanceName} knows about.
</p>
<StyledHTMLTable small striped interactive={false} className="fediverse-sidebar-table">
<StyledHTMLTable condensed striped interactive={false} className="fediverse-sidebar-table">
<tbody>{peerRows}</tbody>
</StyledHTMLTable>
</div>

Wyświetl plik

@ -44,7 +44,7 @@ interface LoginScreenState {
selectedLoginType?: "email" | "fediverseAccount";
error: boolean;
}
class LoginScreen extends React.PureComponent<{}, LoginScreenState> {
class LoginScreen extends React.PureComponent<Record<string, never>, LoginScreenState> {
public constructor(props: any) {
super(props);
this.state = {

Wyświetl plik

@ -1,4 +1,4 @@
import { Button, Callout, H1, InputGroup, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
import { Button, Callout, H1, InputGroup, Intent, NonIdealState, Spinner, SpinnerSize } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { push } from "connected-react-router";
import { get, isEqual } from "lodash";
@ -90,7 +90,7 @@ class SearchScreen extends React.PureComponent<SearchScreenProps, SearchScreenSt
onMouseLeave={this.onMouseLeave}
/>
))}
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
{isLoadingResults && <StyledSpinner size={SpinnerSize.SMALL} />}
{!isLoadingResults && hasMoreResults && (
<Button onClick={this.search} minimal>
Load more results
@ -102,7 +102,7 @@ class SearchScreen extends React.PureComponent<SearchScreenProps, SearchScreenSt
let rightSearchBarElement;
if (isLoadingResults) {
rightSearchBarElement = <Spinner size={Spinner.SIZE_SMALL} />;
rightSearchBarElement = <Spinner size={SpinnerSize.SMALL} />;
} else if (query || error) {
rightSearchBarElement = <Button minimal icon={IconNames.CROSS} onClick={this.clearQuery} aria-label="Search" />;
} else {

Wyświetl plik

@ -20,6 +20,7 @@ export const QUALITATIVE_COLOR_SCHEME = [
"#AD99FF",
"#0E5A8A",
"#0A6640",
"#AAB42F",
"#A66321",
"#A82A2A",
];
@ -56,4 +57,5 @@ export const INSTANCE_TYPES = [
"hubzilla",
"plume",
"wordpress",
"smithereen",
];

Wyświetl plik

@ -19,11 +19,6 @@ import { createBrowserHistory } from "history";
import AppRouter from "./AppRouter";
import createRootReducer from "./redux/reducers";
if (process.env.NODE_ENV === "development") {
const axe = require("react-axe"); // eslint-disable-line @typescript-eslint/no-var-requires
axe(React, ReactDOM, 1000);
}
// https://blueprintjs.com/docs/#core/accessibility.focus-management
FocusStyleManager.onlyShowFocusOnTabs();

Wyświetl plik

@ -1 +0,0 @@
// / <reference types="react-scripts" />

Wyświetl plik

@ -85,38 +85,36 @@ export const loadInstance = (instanceName: string | null) => (dispatch: Dispatch
.catch(() => dispatch(instanceLoadFailed()));
};
export const updateSearch = (query: string, filters: SearchFilter[]) => (
dispatch: Dispatch,
getState: () => AppState
) => {
query = query.trim();
export const updateSearch =
(query: string, filters: SearchFilter[]) => (dispatch: Dispatch, getState: () => AppState) => {
query = query.trim();
if (!query) {
dispatch(resetSearch());
return;
}
if (!query) {
dispatch(resetSearch());
return;
}
const prevQuery = getState().search.query;
const prevFilters = getState().search.filters;
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
const prevQuery = getState().search.query;
const prevFilters = getState().search.filters;
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
const { next } = getState().search;
let url = `search/?query=${query}`;
if (!isNewQuery && next) {
url += `&after=${next}`;
}
const { next } = getState().search;
let url = `search/?query=${query}`;
if (!isNewQuery && next) {
url += `&after=${next}`;
}
// Add filters
// The format is e.g. type_eq=mastodon or user_count_gt=1000
filters.forEach((filter) => {
url += `&${filter.field}_${filter.relation}=${filter.value}`;
});
// Add filters
// The format is e.g. type_eq=mastodon or user_count_gt=1000
filters.forEach((filter) => {
url += `&${filter.field}_${filter.relation}=${filter.value}`;
});
dispatch(requestSearchResult(query, filters));
return getFromApi(url)
.then((result) => dispatch(receiveSearchResults(result)))
.catch(() => dispatch(searchFailed()));
};
dispatch(requestSearchResult(query, filters));
return getFromApi(url)
.then((result) => dispatch(receiveSearchResults(result)))
.catch(() => dispatch(searchFailed()));
};
export const fetchGraph = () => (dispatch: Dispatch) => {
dispatch(requestGraph());
@ -125,22 +123,20 @@ export const fetchGraph = () => (dispatch: Dispatch) => {
.catch(() => dispatch(graphLoadFailed()));
};
export const loadInstanceList = (page?: number, sort?: InstanceSort) => (
dispatch: Dispatch,
getState: () => AppState
) => {
sort = sort || getState().data.instanceListSort;
dispatch(requestInstanceList(sort));
const params: string[] = [];
if (page) {
params.push(`page=${page}`);
}
if (sort) {
params.push(`sortField=${sort.field}`);
params.push(`sortDirection=${sort.direction}`);
}
const path = params ? `instances?${params.join("&")}` : "instances";
return getFromApi(path)
.then((instancesListResponse) => dispatch(receiveInstanceList(instancesListResponse)))
.catch(() => dispatch(instanceListLoadFailed()));
};
export const loadInstanceList =
(page?: number, sort?: InstanceSort) => (dispatch: Dispatch, getState: () => AppState) => {
sort = sort || getState().data.instanceListSort;
dispatch(requestInstanceList(sort));
const params: string[] = [];
if (page) {
params.push(`page=${page}`);
}
if (sort) {
params.push(`sortField=${sort.field}`);
params.push(`sortDirection=${sort.direction}`);
}
const path = params ? `instances?${params.join("&")}` : "instances";
return getFromApi(path)
.then((instancesListResponse) => dispatch(receiveInstanceList(instancesListResponse)))
.catch(() => dispatch(instanceListLoadFailed()));
};

Wyświetl plik

@ -13,7 +13,6 @@ export interface SearchFilter {
type SearchFilterField = "type" | "user_count";
const searchFilterFieldTranslations = {
type: "Instance type",
// eslint-disable-next-line @typescript-eslint/camelcase
user_count: "User count",
};
const searchFilterRelationTranslations = {

Wyświetl plik

@ -1,3 +1,3 @@
declare module "cytoscape-popper" {
const prototype: {};
const prototype: unknown;
}

Wyświetl plik

@ -5,9 +5,9 @@ import { DESKTOP_WIDTH_THRESHOLD, InstanceDomainPath, INSTANCE_DOMAIN_PATH } fro
import { AppState } from "./redux/types";
let API_ROOT = "http://localhost:4000/api/";
if (["true", true, 1, "1"].includes(process.env.REACT_APP_STAGING || "")) {
if (["true", true, 1, "1"].includes(import.meta.env.VITE_STAGING || "")) {
API_ROOT = "https://phoenix.api-develop.fediverse.space/api/";
} else if (process.env.NODE_ENV === "production") {
} else if (import.meta.env.PROD) {
API_ROOT = "https://phoenix.api.fediverse.space/api/";
}

2
frontend/src/vite-env.d.ts vendored 100644
Wyświetl plik

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

Wyświetl plik

@ -1,11 +1,12 @@
{
"compilerOptions": {
"target": "es5",
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"types": ["vite/client"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,

Wyświetl plik

@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), svgr()],
});

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,29 +0,0 @@
[build]
base = "frontend/"
publish = "frontend/build/"
[build.environment]
INLINE_RUNTIME_CHUNK = "false"
[context.develop.environment]
REACT_APP_STAGING = "true"
[context.branch-deploy.environment]
REACT_APP_STAGING = "true"
[context.deploy-preview.environment]
REACT_APP_STAGING = "true"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Content-Type-Options = "nosniff"
X-Frame-Options = "DENY"
X-XSS-Protection = "1"
Content-Security-Policy = "default-src 'self' https://*.fediverse.space https://plausible.cursed.technology; style-src 'self' 'unsafe-inline'; img-src 'self' data:"