kopia lustrzana https://github.com/fediversespace/fediverse.space
Porównaj commity
29 Commity
Autor | SHA1 | Data |
---|---|---|
Tao Bojlén | 6efce18c3b | |
Tao Bojlén | ed0c28f24e | |
Tao Bojlén | de97f6d843 | |
Tao Bojlén | 9c0bf93420 | |
Tao Bojlén | a4eaf75c70 | |
Tao Bojlén | 8f4193e43f | |
Tao Bojlén | 12b035780e | |
Tao Bojlén | 0970e39dea | |
Tao Bojlén | b054f78197 | |
Tao Bojlén | ffa0e50966 | |
Tao Bojlén | f7a5dbc9d5 | |
Tao Bojlén | f6c754a4ac | |
Tao Bojlén | 96a35c5d9a | |
Tao Bojlén | ca831b3831 | |
Tao Bojlén | 01ff551516 | |
Tao Bojlén | 4cdd03dd6c | |
Tao Bojlén | 1ccb2a84a1 | |
Tao Bojlén | ba024c9357 | |
Tao Bojlén | 9cae5e58b2 | |
Tao Bojlén | 4d4193ff49 | |
Tao Bojlén | b7cb7fa685 | |
Tao Bojlén | 332d12e1a4 | |
Tao Bojlén | 98b7448291 | |
Tao Bojlén | 43745881dd | |
Tao Bojlén | 0234a465f2 | |
Tao Bojlén | e7a6c0a988 | |
Tao Bojlén | 9115d29a88 | |
Inex Code | aacc8574d8 | |
Inex Code | 7bb8bd0a8a |
|
@ -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 }}
|
|
@ -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
|
|
@ -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
|
|
@ -1,10 +1,12 @@
|
|||
*.csv
|
||||
.idea/
|
||||
*.gexf
|
||||
data/
|
||||
*.class
|
||||
|
||||
backend/.sobelow
|
||||
node_modules/
|
||||
/frontend/build/
|
||||
/frontend/dist/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
33
README.md
33
README.md
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
#
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"]
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/health OK
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/).
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ##
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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"]
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
119
backend/mix.lock
119
backend/mix.lock
|
@ -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"},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
["other.com"]
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
["other.com"]
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"notesCount": 10,
|
||||
"originalNotesCount": 20,
|
||||
"usersCount": 10,
|
||||
"originalUsersCount": 20,
|
||||
"reactionsCount": 64569657,
|
||||
"instances": 21184,
|
||||
"driveUsageLocal": 0,
|
||||
"driveUsageRemote": 0
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend
|
||||
npx lint-staged
|
9982
frontend/.snyk
9982
frontend/.snyk
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -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>
|
Plik diff jest za duży
Load Diff
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
];
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
// / <reference types="react-scripts" />
|
|
@ -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()));
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
declare module "cytoscape-popper" {
|
||||
const prototype: {};
|
||||
const prototype: unknown;
|
||||
}
|
||||
|
|
|
@ -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/";
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"types": ["vite/client"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
@ -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()],
|
||||
});
|
13672
frontend/yarn.lock
13672
frontend/yarn.lock
Plik diff jest za duży
Load Diff
29
netlify.toml
29
netlify.toml
|
@ -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:"
|
||||
|
Ładowanie…
Reference in New Issue